[深入理解Android卷二 全文-第三章]深入理解SystemServer

阿拉神農發表於2015-08-03

由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容

 

第3章  深入理解SystemServer

本章主要內容:

·  分析SystemServer

·  分析EntropyService、DropBoxManagerService、DiskStatsService

·  分析DeviceStorageMonitorService、SamplingProfilerService以及ClipboardService

本章所涉及的原始碼檔名及位置:

·  SystemServer.java

frameworks/base/services/java/com/android/server/SystemServer.java

·  com_android_server_SystemServer.cpp

frameworks/base/services/jni/com_android_server_SystemServer.cpp

·  System_init.cpp

frameworks/base/cmds/system_server/library/System_init.cpp

·  EntropyService.java

frameworks/base/services/java/com/android/server/EntropyService.java

·  DropBoxManagerService.java

frameworks/base/services/java/com/android/server/DropBoxManagerService.java

·  ActivityManagerService.java

frameworks/base/services/java/com/android/server/am/ActivityManagerService.java

·  DiskStatsService.java

frameworks/base/services/java/com/android/server/DiskStatsService.java

·  dumpsys.cpp

frameworks/base/cmds/dumpsys/dumpsys.cpp

·  DeviceStorageMonitorService.java

frameworks/base/services/java/com/android/server/DeviceStorageMonitorService.java

·  SamplingProfilerService.java

frameworks/base/services/java/com/android/server/SamplingProfilerService.java

·  SamplingProfilerIntegration.java

frameworks/base/core/java/com/android/internal/os/SamplingProfilerIntegration.java

·  SamplingProfiler.java

libcore/dalvik/src/main/java/dalvik/system/profiler/SamplingProfiler.java

·  ClipboardService.java

frameworks/base/services/java/com/android/server/ClipboardService.java

·  ClipboardManager.java(android.content)

frameworks/base/core/java/android/content/ClipboardManager.java

·  ClipboardManager.java(android.text)

frameworks/base/core/java/android/text/ClipboardManager.java

·  ClipData.java

frameworks/base/core/java/android/content/ClipData.java

3.1  概述

SystemServer是什麼?它可是Android Java世界的兩大支柱之一。另外一個支柱是專門負責孵化Java程式的Zygote。這兩大支柱倒了任何一根,都會導致Android Java世界的崩潰(所有由Zygote孵化的Java程式都會被銷燬。SystemServer就是由Zygote孵化而來)。崩潰之後,幸好Linux系統中的天字號程式init會重新啟動它們以重建Java世界。[①]

SystemServer正如其名,和系統服務有著重要關係。Android系統中幾乎所有的核心Service都在這個程式中,如ActivityManagerService、PowerManagerService和WindowManagerService等。那麼,作為這些服務的大本營,SystemServer會是什麼樣的呢?

3.2  SystemServer分析

SystemServer是由Zygote孵化而來的一個程式,通過ps命令,可知其程式名為system_server。

3.2.1  main函式分析

SystemServer核心邏輯的入口是main函式,其程式碼如下:

[-->SystemServer.java]

public static void main(String[] args) {

     if(System.currentTimeMillis() < EARLIEST_SUPPORTED_TIME) {

            //如果系統時鐘早於1970,則設定系統時鐘從1970開始

           Slog.w(TAG, "System clock is before 1970; setting to 1970.");

           SystemClock.setCurrentTimeMillis(EARLIEST_SUPPORTED_TIME);

        }

        //判斷效能統計功能是否開啟

        if(SamplingProfilerIntegration.isEnabled()) {

           SamplingProfilerIntegration.start();

           timer = new Timer();

           timer.schedule(new TimerTask() {

               @Override

               public void run() {

                   //SystemServer效能統計,每小時統計一次,統計結果輸出為檔案

                   SamplingProfilerIntegration.writeSnapshot("system_server",

                                                      null);

               }// SNAPSHOT_INTERVAL定義為1小時

           }, SNAPSHOT_INTERVAL, SNAPSHOT_INTERVAL);

        }

        //和Dalvik虛擬機器相關的設定,主要是記憶體使用方面的控制

       dalvik.system.VMRuntime.getRuntime().clearGrowthLimit();

       VMRuntime.getRuntime().setTargetHeapUtilization(0.8f);

        //載入動態庫libandroid_servers.so

       System.loadLibrary("android_servers");

       init1(args);//呼叫native的init1函式

  }

main函式首先做一些初始化工作,然後載入動態庫libandroid_servers.so,最後再呼叫native的init1函式。該函式在libandroid_servers.so庫中實現,其程式碼如下:

[-->com_android_server_SystemServer.cpp]

extern "C" int system_init();

static voidandroid_server_SystemServer_init1(JNIEnv* env, jobject clazz)

{

   system_init(); //呼叫上面那個用extern 宣告的system_init函式

}

 而system_init函式又在另外一個庫libsystem_server.so中實現,程式碼如下:

[-->System_init.cpp]

extern "C" status_t system_init()

{

     LOGI("Enteredsystem_init()");

     //初始化Binder系統

   sp<ProcessState> proc(ProcessState::self());

     //獲取ServiceManager的客戶端物件BpServiceManager

   sp<IServiceManager> sm = defaultServiceManager();

   

   //GrimReaper是一個很“血腥“的名字,俗稱死神

    sp<GrimReaper>grim = new GrimReaper();

    /*

    下面這行程式碼的作用就是註冊grim物件為ServiceManager死亡資訊的接收者。一旦SM死亡,

    Binder系統就會傳送訃告資訊,這樣grim物件的binderDied函式就會被呼叫。該函式內部

    將kill自己(即SystemServer)。

    筆者覺得,對於這種因摯愛離世而自殺的物體,叫死神好像不太合適

    */

   sm->asBinder()->linkToDeath(grim, grim.get(), 0);

 

    charpropBuf[PROPERTY_VALUE_MAX];

    //判斷SystemServer是否啟動SurfaceFlinger服務,該值由init.rc

    //指令碼設定,預設為零,即不啟動SF服務

    property_get("system_init.startsurfaceflinger",propBuf, "1");

    /*

    從4.0開始,和顯示相關的核心服務surfaceflinger可獨立到另外一個程式中。

    筆者認為,這可能和目前SystemServer的負擔過重有關。另外,隨著智慧終端上HDMI的普及,

    未來和顯示相關的工作將會越來越繁重。將SF放在單獨程式中,不僅可加強集中管理,也可充分

    利用未來智慧終端上多核CPU的資源

    */

    if(strcmp(propBuf, "1") == 0) {

       SurfaceFlinger::instantiate();

    }

    //判斷SystemServer是否啟動感測器服務,預設將啟動感測器服務

   property_get("system_init.startsensorservice", propBuf,"1");

    if(strcmp(propBuf, "1") == 0) {

        //和SF相同,感測器服務也支援在獨立程式中實現

       SensorService::instantiate();

    }

    //獲得AndroidRuntime物件

   AndroidRuntime* runtime = AndroidRuntime::getRuntime();

    JNIEnv*env = runtime->getJNIEnv();

    ......//查詢Java層的SystemServer類,獲取init2函式的methodID

    jclassclazz = env->FindClass("com/android/server/SystemServer");

    ......

   jmethodID methodId = env->GetStaticMethodID(clazz, "init2","()V");

    ......//通過JNI呼叫Java層的init2函式

    env->CallStaticVoidMethod(clazz,methodId);

    //主執行緒加入Binder執行緒池

   ProcessState::self()->startThreadPool();

   IPCThreadState::self()->joinThreadPool();

    returnNO_ERROR;

}

那麼,SystemServer的main函式究竟做了什麼呢?

通過init1函式,辛辛苦苦從Java層穿越到Native層,做了一些初始化工作後,又通過JNI從Native層穿越到Java層去呼叫init2函式。

init2函式返回後,最終又迴歸到Native層。

是不是感覺init1和init2這兩個函式的命名似曾相識,和我們初學程式設計時自定義的函式名非常像呢?其實程式碼中有一段“扭捏”的註釋,解釋了編寫這種“初級”程式碼的原因。很簡單,就是在對AndroidRuntime初始化前必須對一些核心服務初始化。

通過註釋可看出,這段程式碼的作者也擔心被人指責,但至少可以把函式名取得更形象一點吧?

3.2.2  Services群英會

init1函式看起來一點也不復雜,其實好戲都在init2中,其程式碼如下:

[-->SystemServer.java]

public static final void init2() {

        Thread thr = new ServerThread();

       thr.setName("android.server.ServerThread");

       thr.start();//啟動一個執行緒,這個執行緒就像英雄大會一樣,聚集了各路英雄

}

上面的程式碼將建立一個新的執行緒ServerThread,該執行緒的run函式有600多行。如此之長的原因是,Android平臺中眾多Service都彙集於此。先看Services的集體亮相,如圖3-1所示。


圖3-1  Services群英會

圖3-1中有7大類共43個Service(包括Watchdog)。實際上,還有一些Service並沒有在ServerThread的run函式中露面,後面遇到時再做介紹。圖3-1中的7大類服務主要包括:

·  位於第一大類的是Android的核心服務,如ActivityManagerService、WindowManagerService等。

·  位於第二大類的是和通訊相關的服務,如Wifi相關服務、Telephone相關服務。

·  位於第三大類的是和系統功能相關的服務,如AudioService、MountService、UsbService等。

·  位於第四大類的是BatteryService、VibratorService等服務。

·  位於第五大類的是EntropyService,DiskStatsService、Watchdog等相對獨立的服務。

·  位於第六大類的是藍芽服務

·  位於第七大類的是UI方面的服務,如狀態列服務,通知管理服務等。

以上服務的分類並非官方標準,僅是筆者個人之見。

本章將分析其中的第五類服務。該類中的Service之間關係簡單,而且功能相對獨立。第五大類服務包括:

·  EntropyService,熵服務,它和隨機數的生成有關。

·  ClipboardService,剪貼簿服務。

·  DropBoxManagerService,該服務和系統執行時日誌的儲存與管理有關。

·  DiskStatsService以及DeviceStorageMonitorService,這兩個服務用於檢視和監測系統儲存空間。

·  SamplingProfilerService,這個服務是4.0新增的,功能非常簡單。

·  Watchdog,即看門狗,是Android的“老員工”了。我們在卷I第4章“深入理解Zygote”中曾分析過它。Android2.3以後其記憶體檢測功能被去掉,所以與Android 2.2相比,更顯簡單了。這隻小狗很可愛,就留給讀者自己分析了。後面,將逐次分析這第五類服務的其他幾項服務。

3.3  EntropyService分析

根據物理學基本原理,一個系統的熵越大,該系統就越不穩定。在Android中,目前也只有隨機數喜歡處於這種不穩定的系統中了。

SystemServer中新增該服務的程式碼如下:

ServiceManager.addService("entropy", newEntropyService());

上邊程式碼非常簡單,從中可直接分析EntropyService的建構函式:

[-->EntropyService.java]

public EntropyService() {

        //呼叫另外一個建構函式,getSystemDir函式返回的是/data/system目錄

       this(getSystemDir() + "/entropy.dat","/dev/urandom");

}

public EntropyService(String entropyFile, StringrandomDevice) {

   this.randomDevice= randomDevice;//urandom是Linux系統中產生隨機數的裝置

   // /data/system/entropy.dat檔案儲存了系統此前的熵資訊

  this.entropyFile = entropyFile;

  //下面有4個關鍵函式

  loadInitialEntropy();//①

  addDeviceSpecificEntropy();//②

  writeEntropy();//③

  scheduleEntropyWriter();//④

}

從以上程式碼中可以看出,EntropyService建構函式中依次呼叫了4個關鍵函式,這4個函式比較簡單,這裡只介紹它們的作用。感興趣的讀者可自行分析其程式碼。

·  loadInitialEntropy函式:將entropy.dat檔案的中內容寫到urandom裝置,這樣可增加系統的隨機性。根據程式碼中的註釋,系統中有一個entropypool。在系統剛啟動時,該pool中的內容為空,導致早期生成的隨機數變得可預測。通過將entropy.dat資料寫到該entropy pool(這樣該pool中的內容就不為空)中,隨機數的生成就無規律可言了。

·  addDeviceSpecificEntropy函式:將一些和裝置相關的資訊寫入urandom裝置。這些資訊如下:

out.println("Copyright (C) 2009 The AndroidOpen Source Project");

out.println("All Your Randomness Are BelongTo Us");

out.println(START_TIME);

out.println(START_NANOTIME);

out.println(SystemProperties.get("ro.serialno"));

out.println(SystemProperties.get("ro.bootmode"));

out.println(SystemProperties.get("ro.baseband"));

out.println(SystemProperties.get("ro.carrier"));

out.println(SystemProperties.get("ro.bootloader"));

out.println(SystemProperties.get("ro.hardware"));

out.println(SystemProperties.get("ro.revision"));

out.println(new Object().hashCode());

out.println(System.currentTimeMillis());

out.println(System.nanoTime());

該函式的註釋表明,即使向urandom的entropy pool中寫入固定資訊,也能增加隨機數生成的隨機性。從熵的角度考慮,系統的質量越大(即pool中的內容越多),該系統越不穩定。

·  writeEntropy函式:讀取urandom裝置的內容到entropy.dat檔案。

·  scheduleEntropyWriter函式:向EntropyService內部的Handler傳送一個ENTROPY_WHAT訊息。該訊息每3小時傳送一次。收到該訊息後,EntropyService會再次呼叫writeEntropy函式,將urandom裝置的內容寫到entropy.dat中。

通過上面的分析可知,entropy.dat檔案儲存了urandom裝置內容的快照(每三小時更新一次)。當系統重新啟動時,EntropyService又利用這個檔案來增加系統的熵,通過這種方式使隨機數的生成更加不可預測。

EntropyService本身的程式碼很簡單,但是為了儘量保證隨機數的隨機性,Android還是下了一番苦功的。

 

3.4 DropBoxManagerService分析

DropBoxManagerService(簡稱DBMS,下同)用於生成和管理系統執行時的一些日誌檔案。這些日誌檔案大多記錄的是系統或某個應用程式出錯時的資訊。

下面來分析這項服務。其中向SystemServer新增DBMS的程式碼:

ServiceManager.addService(Context.DROPBOX_SERVICE,//服務名為”dropbox”

                           new DropBoxManagerService(context,

                           newFile("/data/system/dropbox")));

 

3.4.1  DBMS建構函式分析

DBMS建構函式如下:

[-->DropBoxManagerService.java]

public DropBoxManagerService(final Contextcontext, File path) {

       mDropBoxDir = path;//path指定dropbox目錄為/data/system/dropbox

       mContext = context;

       mContentResolver = context.getContentResolver();

 

       IntentFilter filter = new IntentFilter();

       filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);

       filter.addAction(Intent.ACTION_BOOT_COMPLETED);

        //註冊一個Broadcast監聽物件,當系統啟動完畢或者裝置儲存空間不足時,會收到廣播

       context.registerReceiver(mReceiver, filter);

       //當Settings資料庫相應項發生變化時候,也需要告知DBMS進行相應處理

       mContentResolver.registerContentObserver(

           Settings.Secure.CONTENT_URI, true,

           new ContentObserver(new Handler()) {

               public void onChange(boolean selfChange) {

             //當Settings資料庫發生變化時候, BroadcastReceiver的onReceive函式

             //將被呼叫。注意第二個引數為null

                   mReceiver.onReceive(context,(Intent) null);

               }

        });

}

根據上面程式碼可知:DBMS註冊一個BroadcastReceiver物件,同時會監聽Settings資料庫的變動。其核心邏輯都在此BroadcastReceiver的onReceive函式中。該函式在以下三種情況發生時被呼叫:

·  當系統啟動完畢時,由BOOT_COMPLETED廣播觸發。

·  當裝置儲存空間不足時,由DEVICE_STORAGE_LOW廣播觸發。

·  當Settings資料庫相應項發生變化時候,該函式也會被觸發。

這個函式內容較簡單,主要功能是儲存空間不足時需要刪除一些老舊的日誌檔案以節省儲存空間。讀者可自行分析這個函式。

 

3.4.2  dropbox日誌檔案的新增

要想理清一個Service,最面好從它提供的服務開始進行分析。根據前面對DBMS的介紹可知,它提供了記錄系統執行時日誌資訊的功能,所以這裡先從dropbox日誌檔案的生成時說起。

當某個應用程式因為發生異常而崩潰(crash)時,ActivityManagerService(簡稱AMS,下同)的handleApplicationCrash函式被呼叫,其程式碼如下:

[-->ActivityManagerService.java]

public void handleApplicationCrash(IBinder app,

                     ApplicationErrorReport.CrashInfocrashInfo) {

   ProcessRecordr = findAppProcess(app, "Crash");

   ......

   //呼叫addErrorToDropBox函式,第一個引數是一個字串,為“crash”

   addErrorToDropBox("crash",r, null, null, null, null, null, crashInfo);

   ......

}

下面來看addErrorToDropBox函式:

[-->ActivityManagerService.java]

public void addErrorToDropBox(String eventType,

           ProcessRecord process, ActivityRecord activity,

            ActivityRecordparent, String subject,

           final String report, final File logFile,

           final ApplicationErrorReport.CrashInfo crashInfo) {

      

    /*

    dropbox日誌檔案的命名有一定的規則,其字首都是一個特定的tag(標籤),

    tag由兩部分組成,合起來是”程式型別”_”事件型別”。

    下邊程式碼中的processClass函式返回該程式的型別,包括“system_server”、“system_app”

    和“data_app”三種。eventType用於指定事件型別,目前也有三種型別:“crash“、”wtf“

    (what aterrible failure)和“anr”

    */

    finalString dropboxTag = processClass(process) + "_" + eventType;

    //獲取DBMS Bn端的物件DropBoxManager

       final DropBoxManager dbox = (DropBoxManager)

               mContext.getSystemService(Context.DROPBOX_SERVICE);

     /*

      對於DBMS,不僅通過tag於標示檔名,還可以根據配置的情況,允許或禁止特定tag日誌

      檔案的記錄。isTagEnable將判斷DBMS是否禁止該標籤,如果該tag已被禁止,則不允許記

      錄日誌檔案

      */

        if(dbox == null || !dbox.isTagEnabled(dropboxTag)) return;

        //建立一個StringBuilder,用於儲存日誌資訊

       final StringBuilder sb = new StringBuilder(1024);

       appendDropBoxProcessHeaders(process, sb);

        ......//將資訊儲存到字串sb中

         //單獨啟動一個執行緒用於向DBMS新增資訊

       Thread worker = new Thread("Error dump: " + dropboxTag) {

           @Override

           public void run() {

               if (report != null) {

                   sb.append(report);

               }

               if (logFile != null) {

                   try {//如果有log檔案,那麼就把log檔案內容讀到sb中

                       sb.append(FileUtils.readTextFile(logFile,

                                128 * 1024,"\n\n[[TRUNCATED]]"));

                   } ......

               }

               //讀取crashInfo資訊,一般記錄的是呼叫堆疊資訊

               if (crashInfo != null && crashInfo.stackTrace != null) {

                   sb.append(crashInfo.stackTrace);

               }

               String setting = Settings.Secure.ERROR_LOGCAT_PREFIX + dropboxTag;

              //查詢Settings資料庫,判斷該tag型別的日誌是否對所記錄的資訊有行數限制,

             //例如某些tag的日誌檔案只准記錄1000行的資訊

              int lines =Settings.Secure.getInt(mContext.getContentResolver(),

                                                       setting, 0);

               if (lines > 0) {

                   sb.append("\n");

                     InputStreamReader input =null;

                   try {

                        //建立一個新程式以執行logcat,後面的引數都是logcat常用的引數

                        java.lang.Processlogcat = new

                          ProcessBuilder("/system/bin/logcat",

                        "-v","time", "-b", "events", "-b","system", "-b",

                             "main", "-t", String.valueOf(lines))

                                .redirectErrorStream(true).start();

                     //由於新程式的輸出已經重定向,因此這裡可以獲取最後lines行的資訊,

                   //不熟悉ProcessBuidler的讀者可以檢視SDK中關於它的用法說明

                     ......

                  }

               }

              //呼叫DBMS的addText

               dbox.addText(dropboxTag, sb.toString());

           }

        };

        if(process == null || process.pid == MY_PID) {

           worker.run(); //如果是SystemServer程式crash了,則不能在別的執行緒執行

        }else {

           worker.start();

     }

}

由上面程式碼可知,addErrorToDropBox在生成日誌的內容上花了不少工夫,因為這些是最重要的,最後僅呼叫addText函式便將內容傳給DBMS的功能。

addText函式定義在DropBoxManager類中,程式碼如下:

[-->DropBoxManager.java]

public void addText(String tag, String data) {

  /*

   mService和DBMS互動。DBMS對外只提供一個add函式用於日誌新增,而DBM提供了3個函式,

   分別是addText、addData、addFile,以方便我們的使用

  */

   try {mService.add(new Entry(tag, 0, data)); } ......

}

DBM向DBMS傳遞的資料被封裝在一個Entry中。下面來看DBMS的add函式,其程式碼如下:

[-->DropBoxManagerService.java]

public void add(DropBoxManager.Entry entry) {

        Filetemp = null;

       OutputStream output = null;

        finalString tag = entry.getTag();//先取出這個Entry的tag

        try{

           int flags = entry.getFlags();

            ......

            //做一些初始化工作,包括生成dropbox目錄、統計當前已有的dropbox檔案資訊等

           init();

           if (!isTagEnabled(tag)) return;//如果該tag被禁止,則不能生成日誌檔案

           long max = trimToFit();

           long lastTrim = System.currentTimeMillis();

           //BlockSize一般是4KB

           byte[] buffer = new byte[mBlockSize];

           //從Entry中得到一個輸入流。與Java I/O相關的類比較多,且用法非常靈活

           //建議讀者閱讀《Java程式設計思想》中“Java I/O系統”一章

            InputStreaminput = entry.getInputStream();

            ......

           int read = 0;

           while (read < buffer.length) {

               int n = input.read(buffer, read, buffer.length - read);

                if (n <= 0) break;

               read += n;

           }

           //先生成一個臨時檔案,命名方式為”drop執行緒id.tmp”

           temp = new File(mDropBoxDir, "drop" +

                         Thread.currentThread().getId()+ ".tmp");

           int bufferSize = mBlockSize;

           if (bufferSize > 4096) bufferSize = 4096;

           if (bufferSize < 512) bufferSize = 512;

           FileOutputStream foutput = new FileOutputStream(temp);

           output = new BufferedOutputStream(foutput, bufferSize);

            //生成GZIP壓縮檔案

           if (read == buffer.length &&

                 ((flags &DropBoxManager.IS_GZIPPED) == 0)) {

               output = new GZIPOutputStream(output);

               flags = flags | DropBoxManager.IS_GZIPPED;

            }

            /*

                DBMS很珍惜/data分割槽,若所生成檔案的size大於一個BlockSize,

                則一定要先壓縮。

            */

            ......//寫檔案,這段程式碼非常繁瑣,其主要目的是儘量節省儲存空間

            /*

            生成一個EntryFile物件,並儲存到DBMS內部的一個資料容器中。

            DBMS除了負責生成檔案外,還提供查詢的功能,這個功能由getNextEntry完成。

             另外,剛才生成的臨時檔案在createEntry函式中會重命為帶標籤的名字,

            讀者可自行分析createEntry函式

            */

           long time = createEntry(temp, tag, flags);

           temp = null;

           Intent dropboxIntent = new

                         Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);

           dropboxIntent.putExtra(DropBoxManager.EXTRA_TAG, tag);

           dropboxIntent.putExtra(DropBoxManager.EXTRA_TIME, time);

           if (!mBooted) {

               dropboxIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);

           }

           //傳送DROPBOX_ENTRY_ADDED廣播。系統中目前還沒有程式接收該廣播

           mContext.sendBroadcast(dropboxIntent,

                            android.Manifest.permission.READ_LOGS);

        }......

}

上面程式碼中略去了DBMS寫檔案的部分,我們從程式碼註釋中可獲悉,DBMS非常珍惜/data分割槽的空間,每一個日誌檔案都需要考慮壓縮以節省儲存空間。如果說細節體現功力,那麼這正是一個極好的例證。

一個真實裝置上/data/system/dropbox目錄的內容如圖3-2所示。


圖3-2  真實裝置中dropbox目錄的內容

圖3-2中最後一項data_app_anr@1324836096560.txt.gz的大小是6.1KB,該檔案解壓後得到的檔案大小是42kB。看來,壓縮確實節省了不少儲存空間。

另外,我們從圖3-2中還發現了其他不同的tag,如SYSTEM_BOOT、SYSTEM_TOMBSTONE等,這些都是由BootReceiver在收到BOOT_COMPLETE廣播後收集相關資訊並傳遞給DBMS而生成的日誌檔案。

3.4.3  DBMS和settings資料庫

DBMS的執行依賴一些配置項。其實除了DBMS,SystemServer中很多服務都依賴相關的配置選項。這些配置項都是通過SettingsProvider操作Settings資料庫來設定和查詢的。SettingsProvider是系統中很重要的一個APK,如果將其刪除,系統就不能正常啟動。

這裡總結一下和DBMS相關的設定項,具體情況如下(注意,右邊雙引號的內容是該配置項在資料庫中的名字。這些和系統相關的項都在Settings資料庫中的Secure表內):

//用來判斷是否允許記錄該tag型別的日誌檔案。預設是允許生成任何tag型別的檔案

Secure.DROPBOX_TAG_PREFIX+tag: “dropbox:”+tag

//用於控制每個日誌檔案的存活時間,預設是三天。大於三天的日誌檔案就會被刪除以節省空間

Secure.DROPBOX_AGE_SECONDS: ”dropbox_age_seconds”

//用於控制系統儲存的日誌檔案個數,預設是1000個檔案

Secure.DROPBOX_MAX_FILES:”dropbox_max_files”

//用於控制dropbox目錄最多佔儲存空間容量的比例,預設是10%

Secure.DROPBOX_QUOTA_PERCENT:”dropbox_quota_percent”

//不允許dropbox使用的儲存空間的比例,預設是10%,也就是dropbox最多隻能使用90%的空間

Secure.DROPBOX_RESERVE_PERCENT:”dropbox_reserve_percent”

//dropbox最大能使用的空間大小,預設是5MB

Secure.DROPBOX_QUOTA_KB:”dropbox_quota_kb”

感興趣的讀者可以通過adb shell進入/data/data/com.android.providers.settings/databases/目錄,然後利用sqlite3命令操作settings.db,其中有一個Secure表。不過系統中的很多選項在該表中都沒有相關設定,因此實際執行時都會使用程式碼中設定的預設值。

3.5 DiskStatsService和DeviceStorageMonitorService分析

DiskStatsService和DeviceStroageMonitorService與系統內部儲存管理和監控有關。

3.5.1 DiskStatsService分析

DiskStatsService程式碼非常簡單,不過也有一個很有意思的地方,例如:

[-->DiskStatsService.java]

public class DiskStatsService extends Binder

DiskStatsService從Binder派生,卻沒有實現任何介面,也就是說,DiskStatsService沒有任何業務函式可供呼叫。為什麼系統會存在這樣的服務呢?

為了解釋這個問題,有必要先把系統中一個很重要的命令dumpsys請出來。正如其名,這個命令用於列印系統中指定服務的資訊,程式碼如下:

[-->dumpsys.cpp]

int main(int argc, char* const argv[])

{

    //先獲取與ServiceManager程式通訊的BpServiceManager物件

   sp<IServiceManager> sm = defaultServiceManager();

   fflush(stdout);

   

    Vector<String16> services;

   Vector<String16> args;

    if (argc== 1) {//如果輸入引數個數為1,則先查詢在SM中註冊的所有Service

       services = sm->listServices();

        //將service排序

       services.sort(sort_func);

        args.add(String16("-a"));

    } else {

        //指定查詢某個service

       services.add(String16(argv[1]));

        //儲存剩餘引數,以後可以傳給service的dump函式

        for(int i=2; i<argc; i++) {

           args.add(String16(argv[i]));

        }

    }

 

    constsize_t N = services.size();

    ......

    for(size_t i=0; i<N; i++) {

       sp<IBinder> service = sm->checkService(services[i]);

        ......

         //通過Binder呼叫該service的dump函式,將args也傳給dump函式

         int err = service->dump(STDOUT_FILENO,args);

         ......

    }

    return0;

}

從上面程式碼可知,dumpsys通過Binder呼叫某個Service的dump函式。那麼

“dumpsys diskstats”的輸出會是什麼呢?馬上來試試,結果如圖3-3所示。


圖3-3  dumpsys diskstats的結果圖示

圖3-3說明了執行“dumpsysdiskstats”列印了系統中內部儲存裝置的使用情況。dumpsys是工作中常用的命令,建議讀者掌握它的用法。

再來看DiskStatsService的dump函式,程式碼如下:

[-->DiskStatsService.java]

protected void dump(FileDescriptor fd, PrintWriterpw, String[] args) {

       byte[] junk = new byte[512];

        for(int i = 0; i < junk.length; i++) junk[i] = (byte) i; 

        //輸出/data/system/perftest.tmp檔案資訊,輸出後即刪除該檔案

        //目前還不清楚這個檔案由誰生成。從名字上看應該和效能測試有關

        Filetmp = new File(Environment.getDataDirectory(),

                                "system/perftest.tmp");

       FileOutputStream fos = null;

       IOException error = null;

        longbefore = SystemClock.uptimeMillis();

        try{

           fos = new FileOutputStream(tmp);

           fos.write(junk);

        } ......

        longafter = SystemClock.uptimeMillis();

        if(tmp.exists()) tmp.delete();

 

        if(error != null) {

            ......

        }else {

           pw.print("Latency: ");

           pw.print(after - before);

           pw.println("ms [512B Data Write]");

        }

        //列印內部儲存裝置各個分割槽的資訊

       reportFreeSpace(Environment.getDataDirectory(), "Data", pw);

       reportFreeSpace(Environment.getDownloadCacheDirectory(),"Cache", pw);

       reportFreeSpace(new File("/system"), "System", pw);

        //有些廠商還會將/proc/yaffs資訊列印出來

}

從前述程式碼中可發現,DiskStatsService沒有實現任何業務介面,似乎只是為了除錯而存在。所以筆者認為,DiskStatsService的功能完全可以被整合到後面即將介紹的DeviceStorageManagerService類中。總之,本節最重要的就是dumpsys這個命令了,建議讀者一定要掌握它的用法。

3.5.2 DeviceStorageManagerService分析

DeviceStorageManagerService(簡稱DSMS,下同)是用來監測系統內部儲存空間的狀態的,新增該服務的程式碼如下:

//DSMS的服務名為“devicestoragemonitor “

ServiceManager.addService(DeviceStorageMonitorService.SERVICE,

                        newDeviceStorageMonitorService(context));

DSMS的建構函式的程式碼如下:

[-->DeviceStorageManagerService.java]

public DeviceStorageMonitorService(Contextcontext) {

       mLastReportedFreeMemTime = 0;

       mContext = context;

       mContentResolver = mContext.getContentResolver();

       mDataFileStats = new StatFs(DATA_PATH);//獲取data分割槽的資訊

       mSystemFileStats = new StatFs(SYSTEM_PATH);// 獲取system分割槽的資訊

       mCacheFileStats = new StatFs(CACHE_PATH);// 獲取cache分割槽的資訊

        //獲得data分割槽的總大小

       mTotalMemory = ((long)mDataFileStats.getBlockCount() *

                       mDataFileStats.getBlockSize())/100L;

        /*

        建立三個Intent,分別用於通知儲存空間不足、儲存空間恢復正常和儲存空間滿。

        由於設定了REGISTERED_ONLY_BEFORE_BOOT標誌,這3個Intent廣播只能由

        系統服務接收

        */

       mStorageLowIntent = newIntent(Intent.ACTION_DEVICE_STORAGE_LOW);

       mStorageLowIntent.addFlags(

                        Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);

       mStorageOkIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_OK);

       mStorageOkIntent.addFlags(

                   Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);

       mStorageFullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_FULL);

       mStorageFullIntent.addFlags(

                   Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);

       mStorageNotFullIntent = new

                    Intent(Intent.ACTION_DEVICE_STORAGE_NOT_FULL);

       mStorageNotFullIntent.addFlags(

                   Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);

       

        //查詢Settings資料庫中sys_storage_threshold_percentage的值,預設是10,

        //即當/data空間只剩下10%的時候,,認為空間不足

       mMemLowThreshold = getMemThreshold();

       //查詢Settings資料庫中的sys_storage_full_threshold_bytes的值,預設是1MB,

       //即當data分割槽只剩1MB時,就認為空間已滿,剩下的這1MB空間保留給系統自用

       mMemFullThreshold = getMemFullThreshold();

        //檢查記憶體

       checkMemory(true);

}

再來看checkMemory函式,程式碼如下:

 

private final void checkMemory(boolean checkCache){

  if(mClearingCache) {

      ......//如果正在清理空間,則不作處理

     } else{

           restatDataDir();//重新計算三個分割槽的剩餘空間大小

            //如果剩餘空間低於mMemLowThreshold,那麼先清理一次空間

            clearCache();

            //如果空間仍不足,則傳送廣播,並在狀態列上設定一個警告通知

             sendNotification();

             ......

            //如果空間已滿,則呼叫下面這個函式,以傳送一次儲存已滿的廣播

             sendFullNotification();

        } ......

      //DEFAULT_CHECK_INTERVAL為1分鐘,即每一分鐘會觸發一次檢查,似乎有點短

       postCheckMemoryMsg(true, DEFAULT_CHECK_INTERVAL);

}

當空間不足時,DSMS會先嚐試clearCache函式,該函式內部會與PackageManagerService互動,其程式碼如下:

[-->DeviceStorageManagerService.java]

private final void clearCache() {

 if(mClearCacheObserver == null) {

      //建立一個CachePackageDataObserver物件,當PKM清理完空間時會回撥該物件的

      //onRemoveCompleted函式

      mClearCacheObserver = new CachePackageDataObserver();

   }

 mClearingCache= true;//設定mClearingCache的值為true,表示我們正在清理空間

 try {

     //呼叫PKM的freeStorageAndNotify函式以清理空間,這個函式在分析PKM時再介紹

    IPackageManager.Stub.asInterface(

            ServiceManager.getService("package")).

            freeStorageAndNotify(mMemLowThreshold,mClearCacheObserver);

   } ......

}

CachePackageDataObserver是DSMS定義的內部類,其onRemoveCompleted函式很簡單,就是重新傳送訊息,讓DSMS再檢測一次記憶體空間。

DeviceStorageManagerService的功能單一,沒有過載dump函式。而DiskStatsService唯一有用的就是dump功能了。不知Google的工程師為什麼沒有把DeviceStorageManagerService和DiskStatsService的功能整合到一起。

3.6  SamplingProfilerService分析

新增SamplingProfilerService服務的程式碼如下:

ServiceManager.addService("samplingprofiler",//服務名

                            newSamplingProfilerService(context));

3.6.1 SamplingProfilerService建構函式分析

下面給來分析SamplingProfilerService的建構函式,其程式碼如下:

[-->SamplingProfilerService.java]

public SamplingProfilerService(Context context) {

       //註冊一個CotentObserver,用於監測Settings資料庫的變化

       registerSettingObserver(context);

       startWorking(context);//① startWorking函式,見下文的分析

}

先來分析上邊的關鍵點startWorking函式,程式碼如下:

[-->SamplingProfilerService.java]

private void startWorking(Context context) {

      finalDropBoxManager dropbox = //得到DropBoxManager物件

               (DropBoxManager)

                   context.getSystemService(Context.DROPBOX_SERVICE);

        //列舉/data/snapshots目錄下的檔案

       File[] snapshotFiles = new File(SNAPSHOT_DIR).listFiles();

        for(int i = 0; snapshotFiles != null && i < snapshotFiles.length;

             i++) {

            //將這些檔案的內容轉移到dropbox中,然後刪除這個檔案

           handleSnapshotFile(snapshotFiles[i], dropbox);

        }

        //建立一個FileObserver物件監控shots目錄,如果目錄中來了新的檔案,那麼把它們

        //轉移到dropbox中

       snapshotObserver = new FileObserver(SNAPSHOT_DIR, FileObserver.ATTRIB) {

           @Override

           public void onEvent(int event, String path) {

               handleSnapshotFile(new File(SNAPSHOT_DIR, path), dropbox);

           }

        };

        //啟動資料夾監控,採用了Linux平臺的inotify機制,感興趣的讀者可以研究下inotify

       snapshotObserver.startWatching();

}

看完上邊的程式碼,不知讀者是否感到有些詫異。對此,筆者有兩個疑惑:

其一,難道SamplingProfilerService的功能就是將/data/snapshots目錄下的檔案轉移到dropbox中嗎?該服務似乎與效能取樣及統計沒有任何關係!

其二,SamplingProfilerService本身並不提供效能統計的功能,那麼效能取樣與統計檔案由誰生成呢?

第二個問題的答案是SamplingProfilerIntegration,這個類封裝了一個SamplingProfiler(由dalvik虛擬機器提供)物件,並提供了方便利用的函式進行效能統計。(可惜這個類並不是由SDK輸出的,要使用該類,就只能利用原始碼進行編譯。)

3.6.2  SamplingProfilerIntegration介紹

先來看如何使用SamplingProfilerIntegration進行效能統計。系統中有很多重要程式都需要對效能進行分析,比如Zygote,其相關程式碼如下:

 [-->zygoteInit.java]

public static void main(String argv[]) {

try {

            //啟動效能統計

           SamplingProfilerIntegration.start();

           ......//Zygote做自己的工作

            //結束統計並生成結果檔案

           SamplingProfilerIntegration.writeZygoteSnapshot();

           ......

}

先看start函式,程式碼如下:

[-->SamplingProfilerIntegration.java]

public static void start() {

        if(!enabled) {//判斷是否開啟效能統計。enable由誰控制?

           return;

        }

        ......

       ThreadGroup group = Thread.currentThread().getThreadGroup();

       SamplingProfiler.ThreadSet threadSet =

                             SamplingProfiler.newThreadGroupTheadSet(group);

        //建立一個dalvik的SamplingProfiler,我們暫時不會對它進行分析

        samplingProfiler= new SamplingProfiler(samplingProfilerDepth,

                                                        threadSet);

        //啟動統計

       samplingProfiler.start(samplingProfilerMilliseconds);

       startMillis = System.currentTimeMillis();

    }

上邊程式碼中提出了一個問題,即用於判斷是否啟動效能統計的enable變數由誰控制,答案在該類的static語句中,其程式碼如下:

[-->SamplingProfilerIntegration.java]

static {

       samplingProfilerMilliseconds = //取系統屬性,預設值為0,即禁止效能統計

                   SystemProperties.getInt("persist.sys.profiler_ms", 0);

       samplingProfilerDepth =

                   SystemProperties.getInt("persist.sys.profiler_depth", 4);

         //如果samplingProfilerMilliseconds的值大於零,則允許效能統計

        if(samplingProfilerMilliseconds > 0) {

           File dir = new File(SNAPSHOT_DIR);

            ......//建立/data/snapshots目錄,並使其可寫

           if (dir.isDirectory()) {

               snapshotWriter = Executors.newSingleThreadExecutor(

                                                   new ThreadFactory() {

                        public ThreadnewThread(Runnable r) {

                            return newThread(r, TAG);//建立用於輸出統計檔案的工作執行緒

                        }

                   });

               enabled = true;

            } ......

        }else {

           snapshotWriter = null;

           enabled = false;

           Log.i(TAG, "Profiling disabled.");

        }

 }

enable的控制竟然放在static語句中,這表明要使用效能統計,就必須重新啟動要統計的程式。

啟動效能統計後,需要輸出統計檔案,這項工作由writeZygoteSnapshot函式完成,其程式碼如下:

[-->SamplingProfilerIntegration.java]

public static void writeZygoteSnapshot() {

        ......

        //呼叫writeSnapshotFile函式,注意第一個引數為“zygote”,用於表示程式名

       writeSnapshotFile("zygote", null);

       samplingProfiler.shutdown();//關閉統計

       samplingProfiler = null;

       startMillis = 0;

}

writeSnapshotFile函式比較簡單,功能就是在shots目錄下生成一個統計檔案,統計檔案的名稱由兩部分組成,合起來就是“程式名_開始效能統計的時刻.snapshot”。另外,writeSnapshotfile內部會呼叫generateSnapshotHeader函式在該統計檔案檔案頭部寫一些特定的資訊,例如版本號、編譯資訊等。

SamplingProfilerIntegration的核心是SamplingProfiler,這個類定義在libcore/dalvik/src/main/java/dalvik/system/profiler/SamplingProfiler.java檔案中。感興趣的讀者可以研究一下該類的用法。在實際開發中,我一般使用Debug類提供的方法進行效能統計。

 

3.7 ClipboardService分析

ClipboardService(簡稱CBS,下同)是Android系統中的元老級服務了,自Android 1.0起就支援剪貼功能。在Android 4.0中再遇見它時,此功能已有了長足改進。先來看和剪貼功能有關的類的家族圖譜,如圖3-4所示。


圖3-4  和剪貼服務有關的類

由圖3-4可知:

·  在Android 4.0中,原始碼中的content.ClipboardManager類繼承了text.ClipboardManager類。從text.ClipboardManager類的命名中也可看出,早期的剪貼功能只支援文字。ClipboardManager由剪貼簿服務的客戶端使用,在SDK中有相應文件說明。

·  新增一個ClipData類,它就像一個容器,管理儲存在其中的資料資訊。具體的資料資訊儲存在ClipData的成員變數mItems中。該變數是一個Item型別的陣列,每一個Item代表一項資料。

·  ClipDescription類用來描述一個ClipData中的資料型別。目前,Android系統中的剪貼簿支援3種型別的資料(Text、Intent、以及URL列表)。

·  剪貼簿的服務端由ClipboardService實現。

下邊我們通過一個例子來分析CBS。該例子來源於AndroidSDK提供的一段示例程式碼(取自SDK安裝目錄/sample/android-14/NotePad)。

3.7.1  Copy資料到剪貼簿

我們擷取與Copy操作相關的程式碼:

[-->sample]

//首先獲取能與CBS互動的ClipboardManager物件

ClipboardManager clipboard = (ClipboardManager)

                          getSystemService(Context.CLIPBOARD_SERVICE);

//呼叫setPrimaryClip函式,引數是ClipData.newUri函式的返回值

clipboard.setPrimaryClip(ClipData.newUri( 

                                   getContentResolver(),"Note",noteUri));

ClipData的newUri是一個static函式,用於返回一個儲存URI資料型別的ClipData,程式碼如下。根據前文所述可知,ClipData物件裝載的就是可儲存在剪貼簿中的資料。

[-->ClipData.java]

static public ClipData newUri(ContentResolverresolver, CharSequence label,

                                      Uri uri) {

     Itemitem = new Item(uri); //建立一個Item,將Uri直接傳給它的建構函式

    String[] mimeTypes = null;

      /*

        下邊程式碼的功能是獲取這個Uri代表的資料的MIME型別。先嚐試利用ContentResolver

        從ContentProvider那查詢,如果查詢不到,則設定mimeTypes為

        MIMETYPES_TEXT_URILIST,它的定義是new String[“text/uri-list”]

      */

      if("content".equals(uri.getScheme())) {

           String realType = resolver.getType(uri);

           //查詢該uri所指向的資料的mimeTypes

           mimeTypes = resolver.getStreamTypes(uri, "*/*");

           if (mimeTypes == null) {

               if (realType != null) {

                   mimeTypes = new String[] {

                            realType,ClipDescription.MIMETYPE_TEXT_URILIST };

               }

           } else {

               ......

        }

        if(mimeTypes == null) {

           mimeTypes = MIMETYPES_TEXT_URILIST;

        }

        //建立一個ClipData物件

       return new ClipData(label, mimeTypes, item);

}

//ClipData的建構函式

public ClipData(CharSequence label, String[]mimeTypes, Item item) {

       mClipDescription = new ClipDescription(label, mimeTypes);

        ......

       mIcon = null;

       mItems.add(item);//將item物件新增到mItems陣列中

    }

newUri函式的主要功能在於,獲得URI所指向的資料的資料型別。對於使用剪下板服務的程式來說,瞭解剪下板中資料的資料型別相當重要,因為這樣可以判斷自己能否處理這種型別的資料。

URI和MIME的關係: URI指向資料的位置,這和PC機上檔案的儲存位置類似,例如c:/dfp。MIME則表示該資料的資料型別。在Windows平臺上是採用字尾名來表示檔案型別的,前面提到的c盤下的dfp檔案,字尾是wav,表示該檔案是一個wav音訊。

對於剪下板來說,資料來源由URI指定,資料型別由MIME表示,兩者缺一不可。

獲得一個ClipData後,將呼叫setPrimaryClip函式,將資料傳遞到CBS。setPrimaryClip的程式碼如下:

public void setPrimaryClip(ClipData clip) {

   try {

             //跨Binder呼叫,先要把引數打包。有興趣的讀者可以看看writToParcel函式

            getService().setPrimaryClip(clip);

        }catch (RemoteException e) {

   }

}

通過Binder傳送setPrimaryClip請求後,由CBS完成實際功能,程式碼如下:

[-->ClipboardService.java]

public void setPrimaryClip(ClipData clip) {

       synchronized (this) {

        ......

         //許可權檢查,後面會在3.7.3中單獨分析

          checkDataOwnerLocked(clip,Binder.getCallingUid());

         */

        //和許可權相關,後續會分析

       clearActiveOwnersLocked();

        //儲存新的clipData到mPrimaryClip中

       mPrimaryClip = clip;

        / *

           mPrimaryClipListeners是一個RemoteCallbackList陣列,

           當CBS中的ClipData發生變化時,CBS需要向那些監控剪下板的

           客戶端傳送通知。客戶端通過addPrimaryClipChangedListener函式

           註冊回撥

        */

       final int n = mPrimaryClipListeners.beginBroadcast();

        for (int i = 0; i < n; i++) {

        try{

               //通知客戶端,剪下板的內容發生變化

                mPrimaryClipListeners.getBroadcastItem(i).

                                        dispatchPrimaryClipChanged();

               }......

           }

           mPrimaryClipListeners.finishBroadcast();

        }

    }

setPrimaryClip比較簡單。但是由於新增支援Uri和Intent這兩種資料型別,因此在安全性方面還有一些需要考慮的地方。這部分內容我們放到3.7.3小節去分析。

RemoteCallbackList是一個比較重要的常用類,很有必要掌握它的用法。

 

3.7.2  從剪下板Paste資料

我們來看一個示例,程式碼如下所示:

[-->Sample]

final void performPaste() {

    //獲取ClipboardManager物件

   ClipboardManager clipboard = (ClipboardManager)

           getSystemService(Context.CLIPBOARD_SERVICE);

 

    //獲取ContentResolver物件

   ContentResolver cr = getContentResolver();

    //從剪貼簿中取出ClipData   

    ClipDataclip = clipboard.getPrimaryClip();

    if (clip!= null) {

       String text=null;

       String title=null;

        //取剪下板ClipData中的第一項Item

       ClipData.Item item = clip.getItemAt(0);

        /*

            下面這行程式碼取出Item中所包含的Uri。看起來順理成章,其實不然。

            應思考這樣一個問題,為什麼這裡一定是取Uri呢?原因是在本例中,

            copy方和paste方都事先了解ClipData中的資料型別。

            如果paste方不瞭解ClipData中的資料型別,該如何處理?

            一種簡單的方法就是採用if/else的判斷語句。另外還有別的方法,

            下文將做分析。

         */

        Uriuri = item.getUri();

       Cursor orig = cr.query(uri,PROJECTION, null, null,null);

           ......//查詢資料庫並獲取資訊

          orig.close();

           }

        }

        if(text == null) {

           //如果paste方不瞭解ClipData中的資料型別,可呼叫coerceToText

          //函式,強制得到文字型別的資料

           text = item.coerceToText(this).toString();//強制為文字

        }

      ......

}

下面來分析getPrimaryClip函式。

[-->ClipboardManager.java]

public ClipData getPrimaryClip() {

   try {

           //呼叫CBS的getPrimaryClip,並傳遞自己的package名

           return getService().getPrimaryClip(mContext.getPackageName());

        }......

}

[-->ClipboardManagerService.java]

public ClipData getPrimaryClip(String pkg) {

       synchronized (this) {

           //賦予該pkg相應的許可權,後文再作分析

           addActiveOwnerLocked(Binder.getCallingUid(), pkg);

           return mPrimaryClip;//返回ClipData給客戶端

        }

}

在上邊的程式碼註釋中,曾提到coerceToText函式。該函式在paste方不瞭解ClipData中資料型別的情況下,可以強制得到文字型別的資料。對於URI和Intent,這個功能又是如何實現的呢?來看下面的程式碼:

[-->ClipData.java]

public CharSequence coerceToText(Context context){

   //如果該Item已經有mText,則直接返回文字

   if (mText!= null) {

       return mText;

   }

   //如果該Item中的資料是URI型別

   if (mUri!= null) {

       FileInputStream stream = null;

        try{

              /*

                ContentProvider需要實現openTypedAssetFileDescriptor函式,

                以返回指定MIME(這裡是text/*)型別的資料來源(AssetFileDescriptor)

              */

              AssetFileDescriptor descr = context.getContentResolver()

                           .openTypedAssetFileDescriptor(mUri, "text/*", null);

              //建立一個輸入流

              stream = descr.createInputStream();

              //建立一個InputStreamReader,讀出來的資料將轉換成UTF-8的文字

              InputStreamReader reader = new InputStreamReader(stream,

                                                      "UTF-8");

              StringBuilder builder = new StringBuilder(128);

              char[] buffer = new char[8192];

              int len;

              //從ContentProvider那讀取資料,然後轉換成UTF-8的字串

              while ((len=reader.read(buffer)) > 0) {

                   builder.append(buffer, 0, len);

              }

              //返回String

              return builder.toString();

         } ......

   }

    //如果是Intent,則呼叫toUri返回一個字串

   if (mIntent != null) {

      returnmIntent.toUri(Intent.URI_INTENT_SCHEME);

     }

   return"";

  }

}

分析上邊程式碼可知,針對URI型別的資料,coerceToText函式還是做了不少工作的。當然,還需要提供該URI的ContentProvider實現相應的函式。

3.7.3  CBS中的許可權管理

在前文的分析中,我們略去了CBS中與許可權管理相關的部分,本節將集中討論這個問題。先來回顧一下CBS中和許可權管理相關的函式呼叫。

//copy方設定ClipData在CBS的setPrimaryClip函式中進行:

checkDataOwnerLocked(clip,Binder.getCallingUid());

clearActiveOwnersLocked();

//paste方獲取ClipData在CBS的getPrimaryClip函式中進行:

addActiveOwnerLocked(Binder.getCallingUid(), pkg);

在分析這3個函式之前,不妨先介紹一下Android系統中的URI許可權管理。

1.  URI許可權管理介紹

Android系統的許可權管理中有一類是專門針對URI的,先來看一個示例,該例來自package/providers/ContactsProvider,在它的AndroidManifest.xml中有如下宣告:

[-->AndroidManifest.xml]

<providerandroid:name="ContactsProvider2"

           ......

           android:readPermission="android.permission.READ_CONTACTS"

           android:writePermission="android.permission.WRITE_CONTACTS">

            ......

           <grant-uri-permission android:pathPattern=".*" />

</provider>

這裡宣告瞭一個名為“ContactsProvider2“的ContentProvider,並定義了幾個許可權宣告,下面對其進行解釋。

·  readPermission:要求呼叫query函式的客戶端必須宣告use-permission為READ_CONTACTS。

·  writePermission:要求呼叫update或insert函式的客戶端必須宣告use-permission為WRITE_CONTACTS。

·  grant-uri-permission:和授權有關。

初識grant-uri-permission時,會覺得比較難以理解,現在通過舉例分析幫助讀者加深認識。

·  Contacts和ContactProvider這兩個APP都是由系統提供的程式,而且二者關係緊密,所以Contacts一定會宣告READ_CONTACTS和WRITE_CONTACT的許可權。如此,Contacts就可以毫無阻礙地通過ContactsProvider來查詢或更新資料庫了。

·  假設Contacts新增一個功能,將ContactsProvider中的某條資料copy到剪下板。根據前面已介紹過的知識可以知道,Contacts會向剪下板中複製一個URI型別的資料。

·  另外一個程式從剪下板中paste這條資料,由於是URI型別的,所以此程式會通過ContentResolver來query該URI所指向的資料。但是這個程式卻並未宣告READ_CONTACTS的許可權,所以它query資料時必然會失敗。

或許有人會問,為什麼第三個程式不宣告相應許可權呢?原因很簡單,第三個程式不知道自己該申明怎樣的許可權(除非這兩個程式的開發者能互通訊息)。本例ContactsProvider設定的讀許可權是READ_CONTACTS,以後可能換成READ_CONTACTS_EXTEND。為了解決類似問題,Android提供了一套專門針對URI的許可權管理機制。以這套機制解決示例中許可權宣告問題的方法是這樣的:當第三個程式從剪下板中paste資料時,系統會判斷是否需要為這個程式授權。當然,系統不會隨意授權,而是需要考慮ContactsProvider的情況。因為ContactsProvider宣告瞭grant-uri-permission,並且所paste的URI匹配其中的pathPattern,所以授權就能成功。倘若ContactsProvider沒有宣告grant-uri-permission,或者URI不匹配指定的pathPattern,則授權失敗。

有了前面介紹的許可權管理機制,相信下面CBS中的許可權管理理解起來就比較簡單了。

感興趣的讀者可閱讀SDK安裝目錄下/docs/guide/topics/security/security.html中關於URI Permission的說明部分。

2. checkDataOwnerLocked函式分析

checkDataOwnerLocked函式的程式碼如下:

[-->ClipboardService.java]

private final void checkDataOwnerLocked(ClipDatadata, int uid) {

        //第二個引數uid為copy方程式的uid

       final int N = data.getItemCount();

        for(int i=0; i<N; i++) {

           //為每一個item呼叫checkItemOwnerLocked

           checkItemOwnerLocked(data.getItemAt(i), uid);

        }

}

// checkItemOwnerLocked函式分析

private final voidcheckItemOwnerLocked(ClipData.Item item, int uid) {

        if(item.getUri() != null) {//檢查Uri

           checkUriOwnerLocked(item.getUri(), uid);

        }

       Intent intent = item.getIntent();

       //getData函式返回的也是一個URI,因此這裡實際上檢查的也是URI

        if(intent != null && intent.getData() != null) {

           checkUriOwnerLocked(intent.getData(), uid);

        }

}

許可權檢查就是針對URI的,因為URI所指向的資料可能是系統內部使用或私密的。例如Setting資料中的Secure表,這裡的資料不能隨意訪問。雖然直接使用ContentResolver訪問這些資料時系統會進行許可權檢查,但是由於目前的剪下板服務也支援URI資料型別,所以這裡也需要做檢查,否則惡意程式就能輕鬆讀取私密資訊。

下邊來分析checkUriOwnerLocked函式,其程式碼如下:

[-->ClipboardService.java]

private final void checkUriOwnerLocked(Uri uri,int uid) {

        ......

        longident = Binder.clearCallingIdentity();

       boolean allowed = false;

        try{

           /*

             呼叫ActivityManagerService的checkGrantUriPermission函式,

              該函式內部將檢查copy方是否能被賦予URI_READ許可權。如果不允許,

              該函式會拋SecurityException異常

           */

           mAm.checkGrantUriPermission(uid, null, uri,

                                           Intent.FLAG_GRANT_READ_URI_PERMISSION);

        }catch (RemoteException e) {

        }finally {

           Binder.restoreCallingIdentity(ident);

        }

    }

根據前面的知識,這裡先要檢查copy方是否有讀取URI的許可權。下面來分析paste方的許可權管理

3.  clearActiveOwnersLocked函式分析

clearActiveOwnersLocked函式的程式碼如下:

[-->ClipboardService.java]

private final void addActiveOwnerLocked(int uid,String pkg) {

       PackageInfo pi;

        try{

          /*

           呼叫PackageManagerService的getPackageInfo函式得到相關資訊

            然後做一次安全檢查,如果PacakgeInfo的uid資訊和當前呼叫的uid不一致,

            則拋SecurityException。這個很好理解,因為paste方可以傳遞虛假的

           packagename,但uid是沒法造假的

           */

           pi = mPm.getPackageInfo(pkg, 0);

           if (pi.applicationInfo.uid != uid) {

               throw new SecurityException("Calling uid " + uid

                        + " does not ownpackage " + pkg);

           }

         } ......

        }

        //mActivePermissionOwners用來儲存已經通過安全檢查的package

        if(mPrimaryClip != null && !mActivePermissionOwners.contains(pkg)) {

           //針對ClipData中的每一個Item,都需要呼叫grantItemLocked來檢查許可權

           final int N = mPrimaryClip.getItemCount();

           for (int i=0; i<N; i++) {

               grantItemLocked(mPrimaryClip.getItemAt(i), pkg);

           }//儲存package資訊到mActivePermissionOwners

           mActivePermissionOwners.add(pkg);

        }

}

//grantItemLocked分析

private final void grantItemLocked(ClipData.Itemitem, String pkg) {

        if(item.getUri() != null) {

           grantUriLocked(item.getUri(), pkg);

        } //和copy方一樣,這裡僅檢查URI的情況

       Intent intent = item.getIntent();

        if(intent != null && intent.getData() != null) {

           grantUriLocked(intent.getData(), pkg);

        }

}

再來看grantUriLocked的程式碼:

[-->ClipboardService.java]

private final void grantUriLocked(Uri uri, Stringpkg) {

        longident = Binder.clearCallingIdentity();

        try{

          /*

              呼叫ActivityManagerService的grantUriPermissionFromOwner函式,

              注意第二個引數傳遞的是CBS所在程式的uid。該函式內部也會檢查許可權。

               該函式呼叫成功後,paste方就被授予了對應URI的讀許可權

            */

           mAm.grantUriPermissionFromOwner(mPermissionOwner,

                                    Process.myUid(),pkg, uri,

                                   Intent.FLAG_GRANT_READ_URI_PERMISSION);

        }catch (RemoteException e) {

        } finally {

           Binder.restoreCallingIdentity(ident);

        }

}

既然有授權,那麼客戶端使用完畢後就需要撤銷授權,這個工作是在setPrimaryClip函式的clearActiveOwnersLocked中完成的。當為剪下板設定新的ClipData時,自然需要將與舊ClipData相關的許可權撤銷。讀者可自行分析clearActiveOwnersLocked函式。

 

3.8  本章小結

本章首先分析了SystemServer程式的啟動過程,然後向讀者展示了該程式中所容納的系統核心服務。從總體上來說,這些服務可分為七大類,本章介紹了其中的第五大類服務,這是因為此類服務功能相對單一,依賴關係較為簡單。這幾個服務包括EntropyService、

DropBoxManagerService、DiskStatsService、DeviceStorageMonitorService、ClipboardService和SamplingProfilerService。其中ClipboardService涉及URI許可權管理方面的知識,相對較難,建議讀者仔細閱讀並深入體會。



[①] 關於zygote及它和systemserver之間的關係,建議讀者閱讀本書卷I第4章“深入理解Zygote”。

相關文章