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

因爲《深刻理解Android 卷一》和《深刻理解Android卷二》再也不出版,而知識的傳播不該該由於紙質媒介的問題而中斷,因此我將在osc博客中全文轉發這兩本書的所有內容

 

第3章  深刻理解SystemServer

本章主要內容:

·  分析SystemServer html

·  分析EntropyService、DropBoxManagerService、DiskStatsService java

·  分析DeviceStorageMonitorService、SamplingProfilerService以及ClipboardService android

本章所涉及的源代碼文件名及位置:

·  SystemServer.java sql

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

·  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 app

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」。

相關文章
相關標籤/搜索