[深入理解Android卷二 全文-第四章]深入理解PackageManagerService

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

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

 

第4章  深入理解PackageManagerService

本章主要內容:

詳細分析PackageManagerService

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

·  SystemServer.java

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

·   IPackageManager.aidl

frameworks/base/core/android/java/content/pm/IPackageManager.aidl

·  PackageManagerService.java

frameworks/base/services/java/com/android/server/pm/PackageManagerService.java

·  Settings.java

frameworks/base/services/java/com/android/server/pm/Settings.java

·  SystemUI的AndroidManifest.xml

frameworks/base/package/systemui/AndroidManifest.xml

·  PackageParser.java

frameworks/base/core/java/android/content/pm/PackageParser.java

·  commandline.c

system/core/adb/commandline.c

·  installd.c

frameworks/base/cmds/installd/installd.c

·  commands.c

frameworks/base/cmds/installd/commands.c

·  pm指令碼檔案

frameworks/base/cmds/pm/pm

·  Pm.java

frameworks/base/cmds/pm/src/com/android/commands/pm/Pm.java

·  DefaultContainerService.java

frameworks/base/packages/defaultcontainerservice/src/com/android/defaultcontainerservice/DefaultContainerService.java

·  UserManager.java

frameworks/base/services/java/com/android/server/pm/UserManager.java

·  UserInfo.java

frameworks/base/core/android/java/content/pm/UserInfo.java

4.1  概述

PackageManagerService是本書分析的第一個核心服務,也是Android系統中最常用的服務之一。它負責系統中Package的管理,應用程式的安裝、解除安裝、資訊查詢等。圖4-1展示了PackageManagerService及客戶端的類家族。


圖4-1  PackageManagerService及客戶端類家族

由圖4-1可知:

·  IPackageManager介面類中定義了服務端和客戶端通訊的業務函式,還定義了內部類Stub,該類從Binder派生並實現了IPackageManager介面。

·  PackageManagerService繼承自IPackageManager.Stub類,由於Stub類從Binder派生,因此PackageManagerService將作為服務端參與Binder通訊。

·  Stub類中定義了一個內部類Proxy,該類有一個IBinder型別(實際型別為BinderProxy)的成員變數mRemote,根據第2章介紹的Binder系統的知識,mRemote用於和服務端PackageManagerService通訊。

·  IPackageManager介面類中定義了許多業務函式,但是出於安全等方面的考慮,Android對外(即SDK)提供的只是一個子集,該子集被封裝在抽象類PackageManager中。客戶端一般通過Context的getPackageManager函式返回一個型別為PackageManager的物件,該物件的實際型別是PackageManager的子類ApplicationPackageManager。這種基於介面程式設計的方式,雖然極大降低了模組之間的耦合性,卻給程式碼分析帶來了不小的麻煩。

·  ApplicationPackageManager類繼承自PackageManager類。它並沒有直接參與Binder通訊,而是通過mPM成員變數指向一個IPackageManager.Stub.Proxy型別的物件。

提示讀者在原始碼中可能找不到IPackageManager.java檔案。該檔案在編譯過程中是經aidl工具處理IPackageManager.aidl後得到,最終的檔案位置在Android原始碼/out/target

/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/content/pm/目錄中。如果讀者沒有整體編譯過原始碼,也可使用aidl工具單獨處理IPackageManager.aidl。

aidl工具生成的結果檔案有著相似的程式碼結構。讀者不妨看看下面這個筆者通過編譯生成的IPackageManager.java檔案。注意,aidl工具生成的結果檔案沒有格式縮排,所以看起來慘不忍睹,讀者可用Eclipse中的原始檔格式化命令處理它。

[-->IPackageManager.java]

public interface IPackageManager extendsandroid.os.IInterface {

     //定義內部類Stub,派生自Binder,實現IPackageManager介面

      publicstatic abstract class Stub extends android.os.Binder

                      implements  android.content.pm.IPackageManager {

            privatestatic final java.lang.String DESCRIPTOR =

                                   "android.content.pm.IPackageManager";

            publicStub() {

                 this.attachInterface(this,DESCRIPTOR);

             }

            ......

               //定義Stub的內部類Proxy,實現IPackageManager介面

            privatestatic class Proxy implements

                                   android.content.pm.IPackageManager{

                  //通過mRemote變數和服務端互動

                 private android.os.IBinder mRemote;

                         Proxy(android.os.IBinderremote) {

                         mRemote = remote;

                        }

                           ......

              }

          ......

}

接下來分析PackageManagerService,為書寫方便起見,以後將其簡稱為PKMS。

4.2  初識PackageManagerService

PKMS作為系統的核心服務,由SystemServer建立,相關程式碼如下:

[-->SystemServer.java]

......//ServerThread的run函式

 /*

 4.0新增的一個功能,即裝置加密(encrypting the device),該功能由

 系統屬性vold.decrypt指定。這部分功能比較複雜,本書暫不討論。

 該功能對PKMS的影響就是通過onlyCore實現的,該變數用於判斷是否只掃描系統庫

 (包括APK和Jar包)

 */

 StringcryptState = SystemProperties.get("vold.decrypt");

 booleanonlyCore = false;

 //ENCRYPTING_STATE的值為"trigger_restart_min_framework"

 if(ENCRYPTING_STATE.equals(cryptState)) {

       ......

      onlyCore = true;

 } else if(ENCRYPTED_STATE.equals(cryptState)) {

       ......//ENCRYPTED_STATE的值為"1"

     onlyCore = true;

 }

 //①呼叫PKMS的main函式,第二個引數用於判斷是否為工廠測試,我們不討論的這種情況,

 //假定onlyCore的值為false

 pm =PackageManagerService.main(context,

           factoryTest !=SystemServer.FACTORY_TEST_OFF,onlyCore);

 booleanfirstBoot = false;

 try {

         //判斷本次是否為初次啟動。當Zygote或SystemServer退出時,init會再次啟動

        //它們,所以這裡的FirstBoot是指開機後的第一次啟動

        firstBoot = pm.isFirstBoot();

 }

......

  try {

        //②做dex優化。dex是Android上針對Java位元組碼的一種優化技術,可提高執行效率

       pm.performBootDexOpt();

  }

 ......

  try {

        pm.systemReady();//③通知系統進入就緒狀態

 }

......

}//run函式結束

以上程式碼中共有4個關鍵呼叫,分別是:

·  PKMS的main函式。這個函式是PKMS的核心,稍後會重點分析它。

·  isFirstBoot、performBootDexOpt和systemReady。這3個函式比較簡單。學完本章後,讀者可完全自行分析它們,故這裡不再贅述。

首先分析PKMS的main函式,它是核心函式,此處單獨用一節進行分析。

4.3  PKMS的main函式分析

PKMS的main函式程式碼如下:

[-->PackageManagerService.java]

public static final IPackageManager main(Contextcontext, boolean factoryTest,

           boolean onlyCore) {

        //呼叫PKMS的建構函式,factoryTest和onlyCore的值均為false

       PackageManagerService m = new PackageManagerService(context,

                                            factoryTest, onlyCore);

        //向ServiceManager註冊PKMS

       ServiceManager.addService("package", m);

       return m;

 }

main函式很簡單,只有短短几行程式碼,執行時間卻較長,主要原因是PKMS在其建構函式中做了很多“重體力活”,這也是Android啟動速度慢的主要原因之一。在分析該函式前,先簡單介紹一下PKMS建構函式的功能。

PKMS建構函式的主要功能是,掃描Android系統中幾個目標資料夾中的APK,從而建立合適的資料結構以管理諸如Package資訊、四大元件資訊、許可權資訊等各種資訊。抽象地看,PKMS像一個加工廠,它解析實際的物理檔案(APK檔案)以生成符合自己要求的產品。例如,PKMS將解析APK包中的AndroidManifest.xml,並根據其中宣告的Activity標籤來建立與此對應的物件並加以保管。

PKMS的工作流程相對簡單,複雜的是其中用於儲存各種資訊的資料結構和它們之間的關係,以及影響最終結果的策略控制(例如前面程式碼中的onlyCore變數,用於判斷是否只掃描系統目錄)。曾經閱讀過PKMS的讀者可能會發現,程式碼中大量不同的資料結構以及它們之間的關係會令人大為頭疼。所以,本章除了分析PKMS的工作流程外,也將關注重要的資料結構及它們的作用。

PKMS建構函式的工作流程大體可分三個階段:

·  掃描目標資料夾之前的準備工作。

·  掃描目標資料夾。

·  掃描之後的工作。

該函式涉及到的知識點較多,程式碼段也較長,因此我們將通過分段討論的方法,集中解決相關的重點問題。

4.3.1  建構函式分析之前期準備工作

下面開始分析建構函式第一階段的工作,先看如下所示的程式碼。

[-->PackageManagerService.java::建構函式]

public PackageManagerService(Context context,boolean factoryTest,

                                  booleanonlyCore) {

       ......

        if(mSdkVersion <= 0) {

           /*

             mSdkVersion是PKMS的成員變數,定義的時候進行賦值,其值取自系統屬性

             “ro.build.version.sdk”,即編譯的SDK版本。如果沒有定義,則APK

             就無法知道自己執行在Android哪個版本上

          */

           Slog.w(TAG, "**** ro.build.version.sdk not set!");//列印一句警告

        }

       mContext = context;

        mFactoryTest= factoryTest;//假定為false,即執行在非工廠模式下

       mOnlyCore = onlyCore;//假定為false,即執行在普通模式下

        //如果此係統是eng版,則掃描Package後,不對package做dex優化

       mNoDexOpt ="eng".equals(SystemProperties.get("ro.build.type"));

       //mMetrics用於儲存與螢幕相關的一些屬性,例如螢幕的寬/高尺寸,解析度等資訊

       mMetrics = new DisplayMetrics();

       //Settings是一個非常重要的類,該類用於儲存系統執行過程中的一些設定,

        //下面進行詳細分析        mSettings = new Settings();

       //①addSharedUserLPw是什麼?馬上來分析

       mSettings.addSharedUserLPw("android.uid.system",

               Process.SYSTEM_UID, ApplicationInfo.FLAG_SYSTEM);

       mSettings.addSharedUserLPw("android.uid.phone",

               MULTIPLE_APPLICATION_UIDS  //該變數的預設值是true

                        ? RADIO_UID :FIRST_APPLICATION_UID,

               ApplicationInfo.FLAG_SYSTEM);

       mSettings.addSharedUserLPw("android.uid.log",

               MULTIPLE_APPLICATION_UIDS

                        ? LOG_UID :FIRST_APPLICATION_UID,

               ApplicationInfo.FLAG_SYSTEM);

       mSettings.addSharedUserLPw("android.uid.nfc",

               MULTIPLE_APPLICATION_UIDS

                        ? NFC_UID :FIRST_APPLICATION_UID,

               ApplicationInfo.FLAG_SYSTEM);

       ......//第一段結束

 

剛進入建構函式,就會遇到第一個較為複雜的資料結構Setting及它的addSharedUserLPw函式。Setting的作用是管理Android系統執行過程中的一些設定資訊。到底是哪些資訊呢?來看下面的分析。

1.  初識Settings

先分析addSharedUserLPw函式。此處擷取該函式的呼叫程式碼,如下所示:

mSettings.addSharedUserLPw("android.uid.system",//字串

              Process.SYSTEM_UID, //系統程式使用的使用者id,值為1000

              ApplicationInfo.FLAG_SYSTEM//標誌系統Package

);

以此處的函式呼叫為例,我們為addSharedUserLPw傳遞了3個引數:

第一個是字串“android.uid.system“;第二個是SYSTEM_UID,其值為1000;第三個是FLAG_SYSTEM標誌,用於標識系統Package。

在進入對addSharedUserLPw函式的分析前,先介紹一下SYSTEM_UID 及相關知識。

(1) Android系統中UID/GID介紹

UID為使用者ID的縮寫,GID為使用者組ID的縮寫,這兩個概念均與Linux系統中程式的許可權管理有關。一般說來,每一個程式都會有一個對應的UID(即表示該程式屬於哪個user,不同user有不同許可權)。一個程式也可分屬不同的使用者組(每個使用者組都有對應的許可權)。

提示Linux的UID/GID還可細分為幾種型別,此處我們僅考慮普適意義的UID/GID。

如上所述,UID/GID和程式的許可權有關。在Android平臺中,系統定義的UID/GID在Process.java檔案中,如下所示:

[-->Process.java]

   //系統程式使用的UID/GID,值為1000

   publicstatic final int SYSTEM_UID = 1000;

   //Phone程式使用的UID/GID,值為1001

   publicstatic final int PHONE_UID = 1001;

   //shell程式使用的UID/GID,值為2000

   publicstatic final int SHELL_UID = 2000;

   //使用LOG的程式所在的組的UID/GID為1007

   publicstatic final int LOG_UID = 1007;

   //供WIF相關程式使用的UID/GID為1010

   publicstatic final int WIFI_UID = 1010;

  //mediaserver程式使用的UID/GID為1013

   publicstatic final int MEDIA_UID = 1013;

   //設定能讀寫SD卡的程式的GID為1015

   publicstatic final int SDCARD_RW_GID = 1015;

   //NFC相關的程式的UID/GID為1025

   publicstatic final int NFC_UID = 1025;

   //有許可權讀寫內部儲存的程式的GID為1023

   publicstatic final int MEDIA_RW_GID = 1023;

   //第一個應用Package的起始UID為10000

   publicstatic final int FIRST_APPLICATION_UID = 10000;

   //系統所支援的最大的應用Package的UID為99999

   publicstatic final int LAST_APPLICATION_UID = 99999;

   //和藍芽相關的程式的GID為2000

   publicstatic final int BLUETOOTH_GID = 2000;

對不同的UID/GID授予不同的許可權,接下來就介紹和許可權設定相關的程式碼。

提示讀者可用adb shell(將什麼?)登入到自己的手機,然後用busybox提供的ps命令檢視程式的UID。

下面分析addSharedUserLPw函式,程式碼如下:

[-->Settings.java]

SharedUserSetting addSharedUserLPw(String name,int uid, int pkgFlags) {

      /*

        注意這裡的引數:name為字串”android.uid.system”,uid為1000,pkgFlags為

        ApplicationInfo.FLAG_SYSETM(以後簡寫為FLAG_SYSTEM)

      */

       //mSharedUsers是一個HashMap,key為字串,值為SharedUserSetting物件

       SharedUserSetting s = mSharedUsers.get(name);

        if(s != null) {

           if (s.userId == uid) {

               return s;

           }......

           return null;

        }

        //建立一個新的SharedUserSettings物件,並設定的userId為uid,

       //SharedUserSettings是什麼?有什麼作用?

        s =new SharedUserSetting(name, pkgFlags);

       s.userId = uid;

        if(addUserIdLPw(uid, s, name)) {

           mSharedUsers.put(name, s);//將name與s鍵值對新增到mSharedUsers中儲存

           return s;

        }

       return null;

    }

從以上程式碼可知,Settings中有一個mSharedUsers成員,該成員儲存的是字串與SharedUserSetting鍵值對,也就是說以字串為key得到對應的SharedUserSetting物件。

那麼SharedUserSettings是什麼?它的目的是什麼?來看一個例子。

(2) SharedUserSetting分析

該例子來源於SystemUI的AndroidManifest.xml,如下所示:

[-->SystemUI的AndroidManifest.xml]

<manifestxmlns:android="http://schemas.android.com/apk/res/android"

       package="com.android.systemui"

       coreApp="true"

        android:sharedUserId="android.uid.system"

       android:process="system">

 ......

在xml中,宣告瞭一個名為android:sharedUserId的屬性,其值為“android.uid.system”。sharedUserId看起來和UID有關,確實如此,它有兩個作用:

·  兩個或多個宣告瞭同一種sharedUserIds的APK可共享彼此的資料,並且可執行在同一程式中。

·  更重要的是,通過宣告特定的sharedUserId,該APK所在程式將被賦予指定的UID。例如,本例中的SystemUI宣告瞭system的uid,執行SystemUI的程式就可享有system使用者所對應的許可權(實際上就是將該程式的uid設定為system的uid)了。

提示除了在AndroidManifest.xml中宣告sharedUserId外,APK在編譯時還必須使用對應的證照進行簽名。例如本例的SystemUI,在其Android.mk中需要額外宣告LOCAL_CERTIFICATE := platform,如此,才可獲得指定的UID。

通過以上介紹,讀者能知道如何組織一種資料結構來包括上面的內容。此處有三個關鍵點需注意:

·  XML中sharedUserId屬性指定了一個字串,它是UID的字串描述,故對應資料結構中也應該有這樣一個字串,這樣就把程式碼和XML中的屬性聯絡起來了。

·  在Linux系統中,真正的UID是一個整數,所以該資料結構中必然有一個整型變數。

·  多個Package可宣告同一個sharedUserId,因此該資料結構必然會儲存那些宣告瞭相同sharedUserId的Package的某些資訊。

瞭解了上面三個關鍵點,再來看Android是如何設計相應資料結構的,如圖4-2所示。


圖4-2  SharedUserSetting類的關係圖

由圖4-2可知:

·  Settings類定義了一個mSharedUsers成員,它是一個HashMap,以字串(如“android.uid.system”)為Key,對應的Value是一個SharedUserSettings物件。

·  SharedUserSetting派生自GrantedPermissions類,從GrantedPermissions類的命名可知,它和許可權有關。SharedUserSetting定義了一個成員變數packages,型別為HashSet,用於儲存宣告瞭相同sharedUserId的Package的許可權設定資訊。

·  每個Package有自己的許可權設定。許可權的概念由PackageSetting類表達。該類繼承自PackagesettingBase,而PackageSettingBase又繼承自GrantedPermissions。

·  Settings中還有兩個成員,一個是mUserIds,另一個是mOtherUserIds,這兩位成員的型別分別是ArrayList和SparseArray。其目的是以UID為索引,得到對應的SharedUserSettings物件。在一般情況下,以索引獲取陣列元素的速度,比以key獲取HashMap中元素的速度要快很多。

提示 根據以上對mUserIds和mOtherUserIds的描述,可知這是典型的以空間換時間的做法。

下邊來分析addUserIdLPw函式,它的功能就是將SharedUserSettings物件儲存到對應的陣列中,程式碼如下:

[-->Settings.java]

private boolean addUserIdLPw(int uid, Object obj, Objectname) {

         //uid不能超出限制。Android對UID進行了分類,應用APK所在程式的UID從10000開始,

        //而系統APK所在程式小於10000

        if(uid >= PackageManagerService.FIRST_APPLICATION_UID +

                            PackageManagerService.MAX_APPLICATION_UIDS){

           return false;

        }

 

        if(uid >= PackageManagerService.FIRST_APPLICATION_UID) {

           int N = mUserIds.size();

          //計算索引,其值是uid和FIRST_APPLICATION_UID的差

           final int index = uid - PackageManagerService.FIRST_APPLICATION_UID;

           while (index >= N) {

               mUserIds.add(null);

               N++;

           }

            ......//判斷該索引位置的內容是否為空,為空才儲存

           mUserIds.set(index, obj);//mUserIds儲存應用Package的UID

        }else {

           ......

            mOtherUserIds.put(uid, obj);//系統Package的UID由mOtherUserIds儲存

        }

       return true;

}

至此,對Settings的分析就告一段落了。在這次“行程”中,我們重點分析了UID/GID以及SharedUserId方面的知識,並見識好幾個重要的資料結構。希望讀者通過SystemUI的例項能夠理解這些資料結構存在的目的。

2.  XML檔案掃描

下面繼續分析PKMS的建構函式,程式碼如下:

[-->PackageMangerService.java::建構函式]

       ......//接前一段

       String separateProcesses = //該值和除錯有關。一般不設定該屬性

                          SystemProperties.get("debug.separate_processes");

        if(separateProcesses != null && separateProcesses.length() > 0) {

          ......

        }else {

           mDefParseFlags = 0;

           mSeparateProcesses = null;

        }

        //建立一個Installer物件,該物件和Native程式installd互動,以後分析installd

        //時再來討論它的作用

       mInstaller = new Installer();

       WindowManager wm =  //得到一個WindowManager物件

              (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

 

       Display d = wm.getDefaultDisplay();

       d.getMetrics(mMetrics); //獲取當前裝置的螢幕資訊

       synchronized (mInstallLock) {

       synchronized (mPackages) {

           //建立一個ThreadHandler物件,實際就是建立一個帶訊息迴圈處理的執行緒,該執行緒

           //的工作是:程式的和解除安裝等。以後分析程式安裝時會和它親密接觸

           mHandlerThread.start();

          //以ThreadHandler執行緒的訊息迴圈(Looper物件)為引數建立一個PackageHandler,

           //可知該Handler的handleMessage函式將執行在此執行緒上

           mHandler = new PackageHandler(mHandlerThread.getLooper());

            File dataDir = Environment.getDataDirectory();

          // mAppDataDir指向/data/data目錄

           mAppDataDir = new File(dataDir, "data");

           // mUserAppDataDir指向/data/user目錄

           mUserAppDataDir = new File(dataDir, "user");

           // mDrmAppPrivateInstallDir指向/data/app-private目錄

           mDrmAppPrivateInstallDir = new File(dataDir, "app-private");

           /*

            建立一個UserManager物件,目前沒有什麼作用,但其前途將不可限量。

             根據Google的設想,未來手機將支援多個User,每個User將安裝自己的應用,

             該功能為Andorid智慧手機推向企業使用者打下堅實基礎

            */

           mUserManager = new UserManager(mInstaller, mUserAppDataDir);

           //①從檔案中讀許可權

           readPermissions();

           //②readLPw分析

           mRestoredSettings = mSettings.readLPw();

           long startTime = SystemClock.uptimeMillis();

以上程式碼中建立了幾個物件,此處暫可不去理會它們。另外,以上程式碼中還呼叫了兩個函式,分別是readPermission和Setttings的readLPw,它們有什麼作用呢?下面就展開分析。

(1) readPermissions函式分析

先來分析readPermissions函式,從其函式名可猜測到它和許可權有關,程式碼如下:

[-->PackageManagerService.java]

void readPermissions() {

        // 指向/system/etc/permission目錄,該目錄中儲存了和裝置相關的一些許可權資訊

        FilelibraryDir = new File(Environment.getRootDirectory(),

                                        "etc/permissions");

        ......

        for(File f : libraryDir.listFiles()) {

           //先處理該目錄下的非platform.xml檔案

           if (f.getPath().endsWith("etc/permissions/platform.xml")) {

               continue;

           }

            ......//呼叫readPermissionFromXml解析此XML檔案

           readPermissionsFromXml(f);

        }

       finalFile permFile = new File(Environment.getRootDirectory(),

               "etc/permissions/platform.xml");

        //解析platform.xml檔案,看來該檔案優先順序最高

       readPermissionsFromXml(permFile);

}

懸著的心終於放了下來!readPermissions函式不就是呼叫readPermissionFromXml函式解析/system/etc/permissions目錄下的檔案嗎?這些檔案似乎都是XML檔案。該目錄下都有哪些XML檔案呢?這些XML檔案中有些什麼內容呢?來看一個實際的例子,如圖4-3所示。


圖4-3  /system/etc/permissions目錄下的內容

圖4-3中列出的是本人G7手機上/system/etc/permissions目錄下的內容。在上面的程式碼中,雖然最後才解析platform.xml檔案, 不過此處先分析此檔案其內容如下所示:

[-->platform.xml]

<permissions>

   <!--建立許可權名與gid的對映關係。如下面宣告的BLUTOOTH_ADMIN許可權,它對應的使用者組是

    net_bt_admin。注意,該檔案中的permission標籤只對那些需要通過讀寫裝置(藍芽/camera)

     /建立socket等程式劃分了gid。因為這些許可權涉及和Linux核心互動,所以需要在底層

     許可權(由不同的使用者組界定)和Android層許可權(由不同的字串界定)之間建立對映關係

  -->

 <permission name="android.permission.BLUETOOTH_ADMIN" >

       <group gid="net_bt_admin" />

 </permission>

 <permission name="android.permission.BLUETOOTH" >

       <group gid="net_bt" />

  </permission>

  ......

   <!--

     賦予對應uid相應的許可權。如果下面一行表示uid為shell,那麼就賦予

       它SEND_SMS的許可權,其實就是把它加到對應的使用者組中-->

   <assign-permission name="android.permission.SEND_SMS"uid="shell" />

   <assign-permission name="android.permission.CALL_PHONE"uid="shell" />

   <assign-permission name="android.permission.READ_CONTACTS"uid="shell" />

   <assign-permission name="android.permission.WRITE_CONTACTS"uid="shell" />

    <assign-permissionname="android.permission.READ_CALENDAR" uid="shell" />

......

    <!-- 系統提供的Java庫,應用程式執行時候必須要連結這些庫,該工作由系統自動完成 -->

    <libraryname="android.test.runner"

           file="/system/frameworks/android.test.runner.jar" />

    <library name="javax.obex"

           file="/system/frameworks/javax.obex.jar"/>

</permissions>

platform.xml檔案中主要使用瞭如下4個標籤:

·  permission和group用於建立Linux層gid和Android層pemission之間的對映關係。

·  assign-permission用於向指定的uid賦予相應的許可權。這個許可權由Android定義,用字串表示。

·  library用於指定系統庫。當應用程式執行時,系統會自動為這些程式載入這些庫。

瞭解了platform.xml後,再看其他的XML檔案,這裡以handheld-core-hardware.xml為例進行介紹,其內容如下:

[-->handheld-core-hardware.xml]

<permissions>

   <feature name="android.hardware.camera" />

   <feature name="android.hardware.location" />

   <feature name="android.hardware.location.network" />

   <feature name="android.hardware.sensor.compass" />

   <feature name="android.hardware.sensor.accelerometer" />

   <feature name="android.hardware.bluetooth" />

   <feature name="android.hardware.touchscreen" />

   <feature name="android.hardware.microphone" />

   <feature name="android.hardware.screen.portrait" />

   <feature name="android.hardware.screen.landscape" />

   </permissions>

這個XML檔案包含了許多feature標籤。根據該檔案中的註釋,這些feature用來描述一個手持終端(包括手機、平板電腦等)應該支援的硬體特性,例如支援camera、支援藍芽等。

注意對於不同的硬體特性,還需要包含其他的xml檔案。例如,要支援前置攝像頭,還需要包含android.hardware.camera.front.xml檔案。這些檔案內容大體一樣,都通過feature標籤表明自己的硬體特性。相關說明可參考handheld-core-hardware.xml中的註釋。

有讀者可能會好奇,真實裝置上/system/etc/permission目錄中的檔案是從哪裡的呢?

答案是,在編譯階段由不同硬體平臺根據自己的配置資訊複製相關檔案到目標目錄中得來的。

這裡給出一個例子,如圖4-4所示。


圖4-4  /system/etc/permission目錄中檔案的來源

由圖4-4可知,當編譯的裝置目標為htc-passion時,就會將Android原始碼目錄/frameworks/base/data/etc/下某些和該目標裝置硬體特性匹配的XML檔案複製到最終輸出目錄/system/etc/permissions下。編譯完成後,將生成system映象。把該映象檔案燒到手機中,就成了目標裝置使用的情況了。

注意4.0原始碼中並沒有htc相關的檔案,這是筆者從2.3原始碼中移植過去的。讀者可參考筆者一篇關於如何移植4.0到G7的博文,地址為http://blog.csdn.net/innost/article/details/6977167

瞭解了與XML相關的知識後,再來分析readPermissionFromXml函式。相信聰明的讀者已經知道它的作用了,就是將XML檔案中的標籤以及它們之間的關係轉換成程式碼中的相應資料結構,程式碼如下:

[-->PackageManagerService.java]

private void readPermissionsFromXml(File permFile){

       FileReader permReader = null;

        try{

           permReader = new FileReader(permFile);

        } ......

        try{

           XmlPullParser parser = Xml.newPullParser();

           parser.setInput(permReader);

           XmlUtils.beginDocument(parser, "permissions");

           while (true) {

               ......

               String name = parser.getName();

               //解析group標籤,前面介紹的XML檔案中沒有單獨使用該標籤的地方

               if ("group".equals(name)) {

                   String gidStr = parser.getAttributeValue(null, "gid");

                   if (gidStr != null) {

                        int gid =Integer.parseInt(gidStr);

                        //轉換XML中的gid字串為整型,並儲存到mGlobalGids中

                        mGlobalGids =appendInt(mGlobalGids, gid);

                   } ......

               } else if ("permission".equals(name)) {//解析permission標籤

                   String perm = parser.getAttributeValue(null, "name");

                  ......

                   perm = perm.intern();

                     //呼叫readPermission處理

                   readPermission(parser, perm);

                 //下面解析的是assign-permission標籤

                } else if("assign-permission".equals(name)) {

                   String perm = parser.getAttributeValue(null, "name");

                   ......

                   String uidStr = parser.getAttributeValue(null, "uid");

                   ......

                   //如果是assign-permission,則取出uid字串,然後獲得Linux平臺上

                   //的整型uid值

                   int uid = Process.getUidForName(uidStr);

                  ......

                   perm = perm.intern();

                   //和assign相關的資訊儲存在mSystemPermissions中

                   HashSet<String> perms = mSystemPermissions.get(uid);

                   if (perms == null) {

                        perms = newHashSet<String>();

                       mSystemPermissions.put(uid, perms);

                   }

                   perms.add(perm);......

                  } else if ("library".equals(name)) {//解析library標籤

                   String lname = parser.getAttributeValue(null, "name");

                   String lfile = parser.getAttributeValue(null, "file");

                   if (lname == null) {

                        ......

                   } else if (lfile == null) {

                        ......

                   } else {

                        //將XML中的name和library屬性值儲存到mSharedLibraries中

                       mSharedLibraries.put(lname,lfile);

                   } ......

               } else if ("feature".equals(name)) {//解析feature標籤

                   String fname = parser.getAttributeValue(null, "name");

                    ......{

                        //在XML中定義的feature由FeatureInfo表達

                        FeatureInfo fi = newFeatureInfo();

                        fi.name = fname;

                        //儲存feature名和對應的FeatureInfo到mAvailableFeatures中

                       mAvailableFeatures.put(fname, fi);

                   }......

                } ......

        } ......

    }

readPermissions函式果然將XML中的標籤轉換成對應的資料結構。總結相關的資料結構,如圖4-4所示,此處借用了UML類圖。在每個類圖中,首行是資料結構名,第二行是資料結構的型別,第三行是註釋。


圖4-4  通過readPermissions函式建立的資料結構及其關係

這裡必須再次強調:圖4-4中各種資料結構的目的是為了儲存XML中各種標籤及它們之間的關係。在分析過程中,最重要的是理解各種標籤的作用,而不是它們所使用的資料結構。

(2) readLPw的“佐料”

readLPw函式的功能也是解析檔案,不過這些檔案的內容卻是在PKMS正常啟動後生成的。這裡僅介紹作為readLPw“佐料”的檔案的資訊。檔案的具體位置在Settings建構函式中指明,其程式碼如下:

[-->Settings.java]

Settings() {

        FiledataDir = Environment.getDataDirectory();

        FilesystemDir = new File(dataDir, "system");//指向/data/system目錄

       systemDir.mkdirs();//建立該目錄

        ......

        /*

        一共有5個檔案,packages.xml和packages-backup.xml為一組,用於描述系統中

        所安裝的Package的資訊,其中backup是臨時檔案。PKMS先把資料寫到backup中,

        資訊都寫成功後再改名成非backup的檔案。其目的是防止在寫檔案過程中出錯,導致資訊丟失。

         packages-stopped.xml和packages-stopped-backup.xml為一組,用於描述系統中

         強制停止執行的pakcage的資訊,backup也是臨時檔案。如果此處存在該臨時檔案,表明

        此前系統因為某種原因中斷了正常流程

        packages.list列出當前系統中應用級(即UID大於10000)Package的資訊

        */

       mSettingsFilename = new File(systemDir, "packages.xml");

       mBackupSettingsFilename = new File(systemDir,"packages-backup.xml");

       mPackageListFilename = new File(systemDir, "packages.list");

       mStoppedPackagesFilename = new File(systemDir,"packages-stopped.xml");

       mBackupStoppedPackagesFilename = new File(systemDir,

                                            "packages-stopped-backup.xml");

}

上面5個檔案共分為三組,這裡簡單介紹一下這些檔案的來歷(不考慮臨時的backup檔案)。

·  packages.xml: PKMS掃描完目標資料夾後會建立該檔案。當系統進行程式安裝、解除安裝和更新等操作時,均會更新該檔案。該檔案儲存了系統中與package相關的一些資訊。

·  packages.list:描述系統中存在的所有非系統自帶的APK的資訊。當這些程式有變動時,PKMS就會更新該檔案。

·  packages-stopped.xml:從系統自帶的設定程式中進入應用程式頁面,然後在選擇強制停止(ForceStop)某個應用時,系統會將該應用的相關資訊記錄到此檔案中。也就是該檔案儲存系統中被使用者強制停止的Package的資訊。

readLPw的函式功能就是解析其中的XML檔案的內容,然後建立並更新對應的資料結構,例如停止的package重啟之後依然是stopped狀態。

提示讀者看完本章後,可自行分析該函式。在此之前,建議讀者不必關注該函式。

3.  第一階段工作總結

在繼續征程前,先總結一下PKMS建構函式在第一階段的工作,千言萬語匯成一句話:掃描並解析XML檔案,將其中的資訊儲存到特定的資料結構中。

第一階段掃描的XML檔案與許可權及上一次掃描得到的Package資訊有關,它為PKMS下一階段的工作提供了重要的參考資訊。

4.3.2  建構函式分析之掃描Package

PKMS建構函式第二階段的工作就是掃描系統中的APK了。由於需要逐個掃描檔案,因此手機上裝的程式越多,PKMS的工作量越大,系統啟動速度也就越慢。

1.  系統庫的dex優化

接著對PKMS建構函式進行分析,程式碼如下:

[-->PackageManagerService.java]

......

 mRestoredSettings= mSettings.readLPw();//接第一段的結尾

 longstartTime = SystemClock.uptimeMillis();//記錄掃描開始的時間

//定義掃描引數

 intscanMode = SCAN_MONITOR | SCAN_NO_PATHS | SCAN_DEFER_DEX;

 if(mNoDexOpt) {

    scanMode|= SCAN_NO_DEX; //在控制掃描過程中是否對APK檔案進行dex優化

  }

 finalHashSet<String> libFiles = new HashSet<String>();

 // mFrameworkDir指向/system/frameworks目錄

 mFrameworkDir = newFile(Environment.getRootDirectory(),"framework");

 // mDalvikCacheDir指向/data/dalvik-cache目錄

 mDalvikCacheDir= new File(dataDir, "dalvik-cache");

 booleandidDexOpt = false;

 /*

  獲取Java啟動類庫的路徑,在init.rc檔案中通過BOOTCLASSPATH環境變數輸出,該值如下

  /system/framework/core.jar:/system/frameworks/core-junit.jar:

  /system/frameworks/bouncycastle.jar:/system/frameworks/ext.jar:

  /system/frameworks/framework.jar:/system/frameworks/android.policy.jar:

  /system/frameworks/services.jar:/system/frameworks/apache-xml.jar:

  /system/frameworks/filterfw.jar

  該變數指明瞭framework所有核心庫及檔案位置

 */

 StringbootClassPath = System.getProperty("java.boot.class.path");

 if(bootClassPath != null) {

     String[] paths = splitString(bootClassPath, ':');

      for(int i=0; i<paths.length; i++) {

        try{  //判斷該jar包是否需要重新做dex優化

             if (dalvik.system.DexFile.isDexOptNeeded(paths[i])) {

                  /*

                   將該jar包檔案路徑儲存到libFiles中,然後通過mInstall物件傳送

                   命令給installd,讓其對該jar包進行dex優化

                  */

                  libFiles.add(paths[i]);

                  mInstaller.dexopt(paths[i], Process.SYSTEM_UID, true);

                  didDexOpt = true;

                }

              } ......

           }

      } ......

   /*

    讀者還記得mSharedLibrarires的作用嗎?它儲存的是platform.xml中宣告的系統庫的資訊。

    這裡也要判斷系統庫是否需要做dex優化。處理方式同上

   */

if (mSharedLibraries.size() > 0) {

     ......

 }

     //將framework-res.apk新增到libFiles中。framework-res.apk定義了系統常用的

    //資源,還有幾個重要的Activity,如長按Power鍵後彈出的選擇框

    libFiles.add(mFrameworkDir.getPath() + "/framework-res.apk");

     //列舉/system/frameworks目錄中的檔案

    String[] frameworkFiles = mFrameworkDir.list();

     if(frameworkFiles != null) {

         ......//判斷該目錄下的apk或jar檔案是否需要做dex優化。處理方式同上

 }

   /*

   上面程式碼對系統庫(BOOTCLASSPATH指定,或 platform.xml定義,或

  /system/frameworks目錄下的jar包與apk檔案)進行一次仔細檢查,該優化的一定要優化。

  如果發現期間對任何一個檔案進行了優化,則設定didDexOpt為true

  */

     if (didDexOpt) {

      String[] files = mDalvikCacheDir.list();

        if (files != null) {

          /*

         如果前面對任意一個系統庫重新做過dex優化,就需要刪除cache檔案。原因和

         dalvik虛擬機器的執行機制有關。本書暫不探討dex及cache檔案的作用。

         從刪除cache檔案這個操作來看,這些cache檔案應該使用了dex優化後的系統庫

         所以當系統庫重新做dex優化後,就需要刪除舊的cache檔案。可簡單理解為快取失效

        */

             for (int i=0; i<files.length; i++) {

                   String fn = files[i];

                     if(fn.startsWith("data@app@")

                          ||fn.startsWith("data@app-private@")) {

                          (newFile(mDalvikCacheDir, fn)).delete();

        ......

 }

2.  掃描系統Package

清空cache檔案後,PKMS終於進入重點段了。接下來看PKMS第二階段工作的核心內容,即掃描Package,相關程式碼如下:

[-->PackageManagerService.java]

   //建立資料夾監控物件,監視/system/frameworks目錄。利用了Linux平臺的inotify機制

  mFrameworkInstallObserver = new AppDirObserver(

                      mFrameworkDir.getPath(),OBSERVER_EVENTS, true);

   mFrameworkInstallObserver.startWatching();

 /*

  呼叫scanDirLI函式掃描/system/frameworks目錄,這個函式很重要,稍後會再分析。

  注意,在第三個引數中設定了SCAN_NO_DEX標誌,因為該目錄下的package在前面的流程

  中已經過判斷並根據需要做過dex優化了

 */

   scanDirLI(mFrameworkDir, PackageParser.PARSE_IS_SYSTEM

            | PackageParser.PARSE_IS_SYSTEM_DIR,scanMode | SCAN_NO_DEX, 0);

     //建立資料夾監控物件,監視/system/app目錄

   mSystemAppDir = new File(Environment.getRootDirectory(),"app");

   mSystemInstallObserver = new AppDirObserver(

               mSystemAppDir.getPath(), OBSERVER_EVENTS, true);

    mSystemInstallObserver.startWatching();

 //掃描/system/app下的package

   scanDirLI(mSystemAppDir, PackageParser.PARSE_IS_SYSTEM

                   | PackageParser.PARSE_IS_SYSTEM_DIR, scanMode, 0);

     //監視並掃描/vendor/app目錄

   mVendorAppDir = new File("/vendor/app");

   mVendorInstallObserver = new AppDirObserver(

               mVendorAppDir.getPath(), OBSERVER_EVENTS, true);

    mVendorInstallObserver.startWatching();

    //掃描/vendor/app下的package

   scanDirLI(mVendorAppDir, PackageParser.PARSE_IS_SYSTEM

                   | PackageParser.PARSE_IS_SYSTEM_DIR, scanMode, 0);

    //和installd互動。以後單獨分析installd

   mInstaller.moveFiles();

由以上程式碼可知,PKMS將掃描以下幾個目錄。

·  /system/frameworks:該目錄中的檔案都是系統庫,例如framework.jar、services.jar、framework-res.apk。不過scanDirLI只掃描APK檔案,所以framework-res.apk是該目錄中唯一“受寵”的檔案。

·  /system/app:該目錄下全是預設的系統應用,例如Browser.apk、SettingsProvider.apk等。

·  /vendor/app:該目錄中的檔案由廠商提供,即廠商特定的APK檔案,不過目前市面上的廠商都把自己的應用放在/system/app目錄下。

注意本書把這三個目錄稱為系統Package目錄,以區分後面的非系統Package目錄。

PKMS呼叫scanDirLI函式進行掃描,下面來分析此函式。

(1) scanDirLI函式分析

scanDirLI函式的程式碼如下:

[-->PackageManagerService.java]

private void scanDirLI(File dir, int flags, intscanMode, long currentTime) {

       String[] files = dir.list();//列舉該目錄下的檔案

       ......

        inti;

        for(i=0; i<files.length; i++) {

           File file = new File(dir, files[i]);

           if (!isPackageFilename(files[i])) {

                continue; //根據檔名字尾,判斷是否為APK 檔案。這裡只掃描APK 檔案

           }

           /*

            呼叫scanPackageLI函式掃描一個特定的檔案,返回值是PackageParser的內部類

            Package,該類的例項代表一個APK檔案,所以它就是和APK檔案對應的資料結構

          */

           PackageParser.Package pkg = scanPackageLI(file,

                flags|PackageParser.PARSE_MUST_BE_APK, scanMode, currentTime);

             if (pkg == null && (flags &PackageParser.PARSE_IS_SYSTEM) == 0 &&

               mLastScanError ==PackageManager.INSTALL_FAILED_INVALID_APK) {

               //注意此處flags的作用,只有非系統Package掃描失敗,才會刪除該檔案

               file.delete();

           }

   }

}

接著來分析scanPackageLI函式。PKMS中有兩個同名的scanPackageLI函式,後面會一一見到。先來看第一個也是最先碰到的scanPackageLI函式。

(2) 初會scanPackageLI函式

首次相遇的scanPackageLI函式的程式碼如下:

[-->PackageManagerService.java]

private PackageParser.Package scanPackageLI(FilescanFile, int parseFlags,

                                       int scanMode, long currentTime)

{

     mLastScanError = PackageManager.INSTALL_SUCCEEDED;

      StringscanPath = scanFile.getPath();

     parseFlags |= mDefParseFlags;//預設的掃描標誌,正常情況下為0

      //建立一個PackageParser物件

     PackageParser pp = new PackageParser(scanPath);

     pp.setSeparateProcesses(mSeparateProcesses);// mSeparateProcesses為空

     pp.setOnlyCoreApps(mOnlyCore);// mOnlyCore為false

      /*

       呼叫PackageParser的parsePackage函式解析APK檔案。注意,這裡把代表螢幕

       資訊的mMetrics物件也傳了進去

       */

      finalPackageParser.Package pkg = pp.parsePackage(scanFile,

               scanPath, mMetrics, parseFlags);

        ......

     PackageSetting ps = null;

     PackageSetting updatedPkg;

        ......

      /*

        這裡略去一大段程式碼,主要是關於Package升級方面的工作。讀者可能會比較好奇:既然是

        升級,一定有新舊之分,如果這裡剛解析後得到的Package資訊是新,那麼舊Package

        的資訊從何得來?還記得”readLPw的‘佐料’”這一小節提到的package.xml檔案嗎?此

        檔案中儲存的就是上一次掃描得到的Package資訊。對比這兩次的資訊就知道是否需要做

        升級了。這部分程式碼比較繁瑣,但不影響我們正常分析。感興趣的讀者可自行研究

      */

      //收集簽名資訊,這部分內容涉及signature,本書暫不擬討論[]

      if (!collectCertificatesLI(pp, ps, pkg,scanFile, parseFlags))

           returnnull;

     //判斷是否需要設定PARSE_FORWARD_LOCK標誌,這個標誌針對資原始檔和Class檔案

     //不在同一個目錄的情況。目前只有/vendor/app目錄下的掃描會使用該標誌。這裡不討論

     //這種情況。

      if (ps != null &&!ps.codePath.equals(ps.resourcePath))

           parseFlags|= PackageParser.PARSE_FORWARD_LOCK;

        String codePath = null;

       String resPath = null;

        if((parseFlags & PackageParser.PARSE_FORWARD_LOCK) != 0) {

            ......//這裡不考慮PARSE_FORWARD_LOCK的情況。

        }else {

           resPath = pkg.mScanPath;

        }

       codePath = pkg.mScanPath;//mScanPath指向該APK檔案所在位置

        //設定檔案路徑資訊,codePath和resPath都指向APK檔案所在位置

       setApplicationInfoPaths(pkg, codePath, resPath);

        //呼叫第二個scanPackageLI函式

       return scanPackageLI(pkg, parseFlags, scanMode | SCAN_UPDATE_SIGNATURE,

                                 currentTime);

}

scanPackageLI函式首先呼叫PackageParser對APK檔案進行解析。根據前面的介紹可知,PackageParser完成了從物理檔案到對應資料結構的轉換。下面來分析這個PackageParser。

(3) PackageParser分析

PackageParser主要負責APK檔案的解析,即解析APK檔案中的AndroidManifest.xml程式碼如下:

[-->PackageParser.java]

 publicPackage parsePackage(File sourceFile, String destCodePath,

           DisplayMetrics metrics, int flags) {

       mParseError = PackageManager.INSTALL_SUCCEEDED;

 

       mArchiveSourcePath = sourceFile.getPath();

        ......//檢查是否為APK檔案

      XmlResourceParser parser = null;

       AssetManager assmgr = null;

       Resources res = null;

       boolean assetError = true;

        try{

           assmgr = new AssetManager();

           int cookie = assmgr.addAssetPath(mArchiveSourcePath);

           if (cookie != 0) {

               res = new Resources(assmgr, metrics, null);

               assmgr.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0,

                              0, 0, 0, 0,Build.VERSION.RESOURCES_SDK_INT);

          /*

           獲得一個XML資源解析物件,該物件解析的是APK中的AndroidManifest.xml檔案。

           以後再討論AssetManager、Resource及相關的知識

         */

            parser = assmgr.openXmlResourceParser(cookie,

                                                  ANDROID_MANIFEST_FILENAME);

               assetError = false;

           } ......//出錯處理

       String[] errorText = new String[1];

       Package pkg = null;

       Exception errorException = null;

        try {

           //呼叫另外一個parsePackage函式

           pkg = parsePackage(res, parser, flags, errorText);

        } ......

        ......//錯誤處理

       parser.close();

       assmgr.close();

       //儲存檔案路徑,都指向APK檔案所在的路徑

       pkg.mPath = destCodePath;

       pkg.mScanPath = mArchiveSourcePath;

       pkg.mSignatures = null;

       return pkg;

}

以上程式碼中呼叫了另一個同名的PackageParser函式,此函式內容較長,但功能單一,就是解析AndroidManifest.xml中的各種標籤,這裡只提取其中相關的程式碼:

[-->PackageParser.java]

private Package parsePackage(

       Resources res, XmlResourceParser parser, int flags, String[] outError)

       throws XmlPullParserException, IOException {

       AttributeSet attrs = parser;

 

       mParseInstrumentationArgs = null;

       mParseActivityArgs = null;

        mParseServiceArgs= null;

       mParseProviderArgs = null;

       //得到Package的名字,其實就是得到AndroidManifest.xml中package屬性的值,

       //每個APK都必須定義該屬性

       String pkgName = parsePackageName(parser, attrs, flags, outError);

        ......

        inttype;

       ......

        //以pkgName名字為引數,建立一個Package物件。後面的工作就是解析XML並填充

       //該Package資訊

       finalPackage pkg = new Package(pkgName);

       boolean foundApp = false;

       ......//下面開始解析該檔案中的標籤,由於這段程式碼功能簡單,所以這裡僅列舉相關函式

      while(如果解析未完成){

      ......

       StringtagName = parser.getName(); //得到標籤名

       if(tagName.equals("application")){

          ......//解析application標籤

           parseApplication(pkg,res, parser, attrs, flags, outError);

       } elseif (tagName.equals("permission-group")) {

        ......//解析permission-group標籤

        parsePermissionGroup(pkg, res, parser, attrs, outError);

       } elseif (tagName.equals("permission")) {

        ......//解析permission標籤

        parsePermission(pkg, res, parser, attrs, outError);

       } else if(tagName.equals("uses-permission")){

          //從XML檔案中獲取uses-permission標籤的屬性

          sa= res.obtainAttributes(attrs,

            com.android.internal.R.styleable.AndroidManifestUsesPermission);

          //取出屬性值,也就是對應的許可權使用宣告

         String name = sa.getNonResourceString(com.android.internal.

                               R.styleable.AndroidManifestUsesPermission_name);

          //新增到Package的requestedPermissions陣列

          if(name != null && !pkg.requestedPermissions.contains(name)) {

               pkg.requestedPermissions.add(name.intern());

          }

       }elseif (tagName.equals("uses-configuration")){

        /*

           該標籤用於指明本package對硬體的一些設定引數,目前主要針對輸入裝置(觸控式螢幕、鍵盤

           等)。遊戲類的應用可能對此有特殊要求。

       */

         ConfigurationInfocPref = new ConfigurationInfo();

        ......//解析該標籤所支援的各種屬性

        pkg.configPreferences.add(cPref);//儲存到Package的configPreferences陣列

       }

      ......//對其他標籤解析和處理

}

上面程式碼展示了AndroidManifest.xml解析的流程,其中比較重要的函式是parserApplication,它用於解析application標籤及其子標籤(Android的四大元件在application標籤中已宣告)。

圖4-5表示了PackageParser及其內部重要成員的資訊。


圖4-5  PackageParser大家族

由圖4-5可知:

·  PackageParser定了相當多的內部類,這些內部類的作用就是儲存對應的資訊。解析AndroidManifest.xml檔案得到的資訊由Package儲存。從該類的成員變數可看出,和Android四大元件相關的資訊分別由activites、receivers、providers、services儲存。由於一個APK可宣告多個元件,因此activites和receivers等均宣告為ArrayList。

·  以PackageParser.Activity為例,它從Component<ActivityIntentInfo>派生。Component是一個模板類,元素型別是ActivityIntentInfo,此類的頂層基類是IntentFilter。PackageParser.Activity內部有一個ActivityInfo型別的成員變數,該變數儲存的就是四大元件中Activity的資訊。細心的讀者可能會有疑問,為什麼不直接使用ActivityInfo,而是通過IntentFilter構造出一個使用模板的複雜型別PackageParser.Activity呢?原來,Package除了儲存資訊外,還需要支援Intent匹配查詢。例如,設定Intent的Action為某個特定值,然後查詢匹配該Intent的Activity。由於ActivityIntentInfo是從IntentFilter派生的,因此它它能判斷自己是否滿足該Intent的要求,如果滿足,則返回對應的ActivityInfo。在後續章節會詳細討論根據Intent查詢特定Activity的工作流程。

·  PackageParser定了一個輕量級的資料結構PackageLite,該類僅儲存Package的一些簡單資訊。我們在介紹Package安裝的時候,會遇到PackageLite。

注意讀者需要了解Java泛型類的相關知識。

(4) 與scanPackageLI再相遇

在PackageParser掃描完一個APK後,此時系統已經根據該APK中AndroidManifest.xm,建立了一個完整的Package物件,下一步就是將該Package加入到系統中。此時呼叫的函式就是另外一個scanPackageLI,其程式碼如下:

[-->PackageManagerService.java::scanPackageLI函式]

private PackageParser.PackagescanPackageLI(PackageParser.Package pkg,

           int parseFlags, int scanMode, long currentTime) {

        FilescanFile = new File(pkg.mScanPath);

        ......

       mScanningPath = scanFile;

        //設定package物件中applicationInfo的flags標籤,用於標示該Package為系統

       //Package

        if((parseFlags&PackageParser.PARSE_IS_SYSTEM) != 0) {

           pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;

        }

        //①下面這句if判斷極為重要,見下面的解釋

        if(pkg.packageName.equals("android")) {

           synchronized (mPackages) {

               if (mAndroidApplication != null) {

                  ......

               mPlatformPackage = pkg;

               pkg.mVersionCode = mSdkVersion;

               mAndroidApplication = pkg.applicationInfo;

               mResolveActivity.applicationInfo = mAndroidApplication;

               mResolveActivity.name = ResolverActivity.class.getName();

               mResolveActivity.packageName = mAndroidApplication.packageName;

               mResolveActivity.processName = mAndroidApplication.processName;

               mResolveActivity.launchMode = ActivityInfo.LAUNCH_MULTIPLE;

               mResolveActivity.flags = ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS;

               mResolveActivity.theme =

                          com.android.internal.R.style.Theme_Holo_Dialog_Alert;

               mResolveActivity.exported = true;

               mResolveActivity.enabled = true;

               //mResoveInfo的activityInfo成員指向mResolveActivity

               mResolveInfo.activityInfo = mResolveActivity;

               mResolveInfo.priority = 0;

               mResolveInfo.preferredOrder = 0;

               mResolveInfo.match = 0;

               mResolveComponentName = new ComponentName(

                       mAndroidApplication.packageName, mResolveActivity.name);

           }

        }

剛進入scanPackageLI函式,我們就發現了一個極為重要的內容,即單獨判斷並處理packageName為“android”的Package。和該Package對應的APK是framework-res.apk,有圖為證,如圖4-6所示為該APK的AndroidManifest.xml中的相關內容。


圖4-6  framework-res.apk的AndroidManifest.xml

實際上,framework-res.apk還包含了以下幾個常用的Activity。

·  ChooserActivity:當多個Activity符合某個Intent的時候,系統會彈出此Activity,由使用者選擇合適的應用來處理。

·  RingtonePickerActivity:鈴聲選擇Activity。

·  ShutdownActivity:關機前彈出的選擇對話方塊。

由前述知識可知,該Package和系統息息相關,因此它得到了PKMS的特別青睞,主要體現在以下幾點。

·  mPlatformPackage成員用於儲存該Package資訊。

·  mAndroidApplication用於儲存此Package中的ApplicationInfo。

·  mResolveActivity指向用於表示ChooserActivity資訊的ActivityInfo。

·  mResolveInfo為ResolveInfo型別,它用於儲存系統解析Intent(經IntentFilter的過濾)後得到的結果資訊,例如滿足某個Intent的Activity的資訊。由前面的程式碼可知,mResolveInfo的activityInfo其實指向的就是mResolveActivity。

注意在從PKMS中查詢滿足某個Intent的Activity時,返回的就是ResolveInfo,再根據ResolveInfo的資訊得到具體的Activity。

此處儲存這些資訊,主要是為了提高執行過程中的效率。Goolge工程師可能覺得ChooserActivity使用的地方比較多,所以這裡單獨儲存了此Activity的資訊。

好,繼續對scanPackageLI函式的分析。

[-->PackageManagerService::scanPackageLI函式]

......//mPackages用於儲存系統內的所有Package,以packageName為key

   if(mPackages.containsKey(pkg.packageName)

               || mSharedLibraries.containsKey(pkg.packageName)) {

       return null;

   }

      File destCodeFile = newFile(pkg.applicationInfo.sourceDir);

      FiledestResourceFile = new File(pkg.applicationInfo.publicSourceDir);

      SharedUserSettingsuid = null;//代表該Package的SharedUserSetting物件

     PackageSetting pkgSetting = null;//代表該Package的PackageSetting物件

      synchronized(mPackages) {

         ......//此段程式碼大約有300行左右,主要做了以下幾方面工作

         /*

          ①如果該Packge宣告瞭” uses-librarie”話,那麼系統要判斷該library是否

            在mSharedLibraries中

          ②如果package宣告瞭SharedUser,則需要處理SharedUserSettings相關內容,

            由Settings的getSharedUserLPw函式處理

           ③處理pkgSetting,通過呼叫Settings的getPackageLPw函式完成

           ④呼叫verifySignaturesLP函式,檢查該Package的signature

          */

       }

      finallong scanFileTime = scanFile.lastModified();

      finalboolean forceDex = (scanMode&SCAN_FORCE_DEX) != 0;

      //確定執行該package的程式的程式名,一般用packageName作為程式名

     pkg.applicationInfo.processName = fixProcessName(

                         pkg.applicationInfo.packageName,

                         pkg.applicationInfo.processName,

                         pkg.applicationInfo.uid);

      if(mPlatformPackage == pkg) {

           dataPath = new File (Environment.getDataDirectory(),"system");

           pkg.applicationInfo.dataDir = dataPath.getPath();

        }else {

            /*

             getDataPathForPackage函式返回該package的目錄

            一般是/data/data/packageName/

           */

           dataPath = getDataPathForPackage(pkg.packageName, 0);

            if(dataPath.exists()){

             ......//如果該目錄已經存在,則要處理uid的問題

           } else {

             ......//向installd傳送install命令,實際上就是在/data/data下

                   //建立packageName目錄。後續將分析installd相關知識

               int ret = mInstaller.install(pkgName, pkg.applicationInfo.uid,

                       pkg.applicationInfo.uid);

              //為系統所有user安裝此程式

               mUserManager.installPackageForAllUsers(pkgName,

                                pkg.applicationInfo.uid);

 

               if (dataPath.exists()) {

                   pkg.applicationInfo.dataDir = dataPath.getPath();

               } ......

               

               if (pkg.applicationInfo.nativeLibraryDir == null &&

                      pkg.applicationInfo.dataDir!= null) {

               ......//為該Package確定native library所在目錄

              //一般是/data/data/packagename/lib

           }

}

    //如果該APK包含了native動態庫,則需要將它們從APK檔案中解壓並複製到對應目錄中

    if(pkg.applicationInfo.nativeLibraryDir != null) {

           try {

               final File nativeLibraryDir = new

                            File(pkg.applicationInfo.nativeLibraryDir);

               final String dataPathString = dataPath.getCanonicalPath();

               //從2.3開始,系統package的native庫統一放在/system/lib下。所以

               //系統不會提取系統Package目錄下APK包中的native庫

               if (isSystemApp(pkg) && !isUpdatedSystemApp(pkg)) {

                   NativeLibraryHelper.removeNativeBinariesFromDirLI(

                                            nativeLibraryDir)){

           } else if (nativeLibraryDir.getParentFile().getCanonicalPath()

                       .equals(dataPathString)) {

                   boolean isSymLink;

                   try {

                        isSymLink = S_ISLNK(Libcore.os.lstat(

                                        nativeLibraryDir.getPath()).st_mode);

                   } ......//判斷是否為連結,如果是,需要刪除該連結

                   if (isSymLink) {

                       mInstaller.unlinkNativeLibraryDirectory(dataPathString);

                   }

             //在lib下建立和CPU型別對應的目錄,例如ARM平臺的是arm/,MIPS平臺的是mips/

               NativeLibraryHelper.copyNativeBinariesIfNeededLI(scanFile,

                                   nativeLibraryDir);

               } else {

                   mInstaller.linkNativeLibraryDirectory(dataPathString,

                                       pkg.applicationInfo.nativeLibraryDir);

               }

           } ......

        }

     pkg.mScanPath= path;

     if((scanMode&SCAN_NO_DEX) == 0) {

            ......//對該APK做dex優化

        performDexOptLI(pkg,forceDex, (scanMode&SCAN_DEFER_DEX);

      }

     //如果該APK已經存在,要先殺掉執行該APK的程式

     if((parseFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) {

           killApplication(pkg.applicationInfo.packageName,

                       pkg.applicationInfo.uid);

     }

......

     /*

     在此之前,四大元件資訊都屬於Package的私有財產,現在需要把它們登記註冊到PKMS內部的

     財產管理物件中。這樣,PKMS就可對外提供統一的元件資訊,而不必拘泥於具體的Package

     */

    synchronized(mPackages) {

   if ((scanMode&SCAN_MONITOR) != 0) {

        mAppDirs.put(pkg.mPath, pkg);

   }

  mSettings.insertPackageSettingLPw(pkgSetting, pkg);

   mPackages.put(pkg.applicationInfo.packageName,pkg);

   //處理該Package中的Provider資訊

   int N =pkg.providers.size();

   int i;

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

   PackageParser.Providerp = pkg.providers.get(i);

   p.info.processName=fixProcessName(

                               pkg.applicationInfo.processName,

                  p.info.processName, pkg.applicationInfo.uid);

    //mProvidersByComponent提供基於ComponentName的Provider資訊查詢

    mProvidersByComponent.put(new ComponentName(

                               p.info.packageName,p.info.name), p);

             ......

   }

   //處理該Package中的Service資訊

   N =pkg.services.size();

   r = null;

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

   PackageParser.Service s =pkg.services.get(i);

   mServices.addService(s);

   }

   //處理該Package中的BroadcastReceiver資訊

   N =pkg.receivers.size();

   r = null;

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

   PackageParser.Activity a =pkg.receivers.get(i);

   mReceivers.addActivity(a,"receiver");

   ......

   }

   //處理該Package中的Activity資訊

   N = pkg.activities.size();

   r =null;

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

   PackageParser.Activity a =pkg.activities.get(i);

   mActivities.addActivity(a,"activity");//後續將詳細分析該呼叫

  }

   //處理該Package中的PermissionGroups資訊

   N = pkg.permissionGroups.size();

   ......//permissionGroups處理

   N =pkg.permissions.size();

   ......//permissions處理

   N =pkg.instrumentation.size();

   ......//instrumentation處理

   if(pkg.protectedBroadcasts != null) {

      N = pkg.protectedBroadcasts.size();

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

        mProtectedBroadcasts.add(pkg.protectedBroadcasts.get(i));

      }

}

   ......//Package的私有財產終於完成了公有化改造

return pkg;

}

到此這個長達800行的程式碼就分析完了,下面總結一下Package掃描的流程。

(5) scanDirLI函式總結

scanDirLI用於對指定目錄下的APK檔案進行掃描,如圖4-7所示為該函式的呼叫流程。


圖4-7  scanDirLI工作流程總結

圖4-7比較簡單,相關知識無須贅述。讀者在自行分析程式碼時,只要注意區分這兩個同名scanPackageLI函式即可。

掃描完APK檔案後,Package的私有財產就充公了。PKMS提供了好幾個重要資料結構來儲存這些財產,這些資料結構的相關資訊如圖4-8所示。


圖4-8  PKMS中重要的資料結構

圖4-8借用UML的類圖來表示PKMS中重要的資料結構。每個類圖的第一行為成員變數名,第二行為資料型別,第三行為註釋說明。

3.  掃描非系統Package

非系統Package就是指那些不儲存在系統目錄下的APK檔案,這部分程式碼如下:

[-->PackageManagerService.java::建構函式第三部分]

           if (!mOnlyCore) {//mOnlyCore用於控制是否掃描非系統Package

               Iterator<PackageSetting> psit =  

                            mSettings.mPackages.values().iterator();

               while (psit.hasNext()) {

                   ......//刪除系統package中那些不存在的APK

           }

             mAppInstallDir = new File(dataDir,"app");

            ......//刪除安裝不成功的檔案及臨時檔案

           if (!mOnlyCore) {

              //在普通模式下,還需要掃描/data/app以及/data/app_private目錄 

              mAppInstallObserver = new AppDirObserver(

                   mAppInstallDir.getPath(), OBSERVER_EVENTS, false);

              mAppInstallObserver.startWatching();

              scanDirLI(mAppInstallDir, 0, scanMode, 0);

               mDrmAppInstallObserver = newAppDirObserver(

                   mDrmAppPrivateInstallDir.getPath(), OBSERVER_EVENTS, false);

              mDrmAppInstallObserver.startWatching();

              scanDirLI(mDrmAppPrivateInstallDir,           

                            PackageParser.PARSE_FORWARD_LOCK,scanMode,0);

           } else {

               mAppInstallObserver = null;

               mDrmAppInstallObserver = null;

      }

結合前述程式碼,這裡總結幾個存放APK檔案的目錄。

·  系統Package目錄包括:/system/frameworks、/system/app和/vendor/app。

·  非系統Package目錄包括:/data/app、/data/app-private。

4.  第二階段工作總結

PKMS建構函式第二階段的工作任務非常繁重,要建立比較多的物件,所以它是一個耗時耗記憶體的操作。在工作中,我們一直想優化該流程以加快啟動速度,例如延時掃描不重要的APK,或者儲存Package資訊到檔案中,然後在啟動時從檔案中恢復這些資訊以減少APK檔案讀取並解析XML的工作量。但是一直沒有一個比較完滿的解決方案,原因有很多。比如APK之間有著比較微妙的依賴關係,因此到底延時掃描哪些APK,尚不能確定。另外,筆者感到比較疑惑的一個問題是:對於多核CPU架構,PKMS可以啟動多個執行緒以掃描不同的目錄,但是目前程式碼中還沒有尋找到相關的蛛絲馬跡。難道此處真的就不能優化了嗎?讀者如果有更好的解決方案,不妨和大家分享一下。

4.3.3  建構函式分析之掃尾工作

下面分析PKMS第三階段的工作,這部分任務比較簡單,就是將第二階段收集的資訊再集中整理一次,比如將有些資訊儲存到檔案中,相關程式碼如下:

[-->PackageManagerService.java::建構函式]

......

  mSettings.mInternalSdkPlatform= mSdkVersion;

  //彙總並更新和Permission相關的資訊

  updatePermissionsLPw(null, null, true,

                           regrantPermissions,regrantPermissions);

   //將資訊寫到package.xml、package.list及package-stopped.xml檔案中

   mSettings.writeLPr();

   Runtime.getRuntime().gc();

    mRequiredVerifierPackage= getRequiredVerifierLPr();

......//PKMS建構函式返回

}

讀者可自行研究以上程式碼中涉及的幾個函式,這裡不再贅述。

4.3.4  PKMS建構函式總結

從流程角度看,PKMS建構函式的功能還算清晰,無非是掃描XML或APK檔案,但是其中涉及的資料結構及它們之間的關係卻較為複雜。這裡有一些建議供讀者參考:

·  理解PKMS建構函式工作的三個階段及其各階段的工作職責。

·  瞭解PKMS第二階段工作中解析APK檔案的幾個關鍵步驟,可參考圖4-7。

·  瞭解重點資料結構的名字和大體功能。

如果對PKMS的分析到此為止,則未免有些太小視它了。下面將分析幾個重量級的知識點,期望能帶領讀者全方位認識PKMS。

4.4  APK Installation分析

本節將分析APK的安裝及相關處理流程,它可能比讀者想象得要複雜。

馬上開始我們的行程,故事從adb install開始。

4.4.1  adb install分析

adb install有多個引數,這裡僅考慮最簡單的,如adb installframeworktest.apk。adb是一個命令,install是它的引數。此處直接跳到處理install引數的程式碼:

[-->commandline.c]

int adb_commandline(int argc, char **argv){

   ...... 

if(!strcmp(argv[0], "install")) {

       ......//呼叫install_app函式處理

       return install_app(ttype, serial, argc, argv);

}

......

}

install_app函式也在commandline.c中定義,程式碼如下:

[-->commandline.c]

int install_app(transport_type transport, char*serial, int argc, char** argv)

{

    //要安裝的APK現在還在Host機器上,要先把APK複製到手機中。

   //這裡需要設定複製目標的目錄,如果安裝在內部儲存中,則目標目錄為/data/local/tmp;

   //如果安裝在SD卡上,則目標目錄為/sdcard/tmp。

    staticconst char *const DATA_DEST = "/data/local/tmp/%s";

    staticconst char *const SD_DEST = "/sdcard/tmp/%s";

    constchar* where = DATA_DEST;

    charapk_dest[PATH_MAX];

    charverification_dest[PATH_MAX];

    char*apk_file;

    char*verification_file = NULL;

    intfile_arg = -1;

    int err;

    int i;

    for (i =1; i < argc; i++) {

        if(*argv[i] != '-') {

           file_arg = i;

           break;

        }else if (!strcmp(argv[i], "-i")) {

            i++;

        }else if (!strcmp(argv[i], "-s")) {

           where = SD_DEST; //-s引數指明該APK安裝到SD卡上

        }

    }

    ......

    apk_file= argv[file_arg];

    ......

    //獲取目標檔案的全路徑,如果安裝在內部儲存中,則目標全路徑為/data/local/tmp/安裝包名,

    //呼叫do_sync_push將此APK傳送到手機的目標路徑

    err =do_sync_push(apk_file, apk_dest, 1 /* verify APK */);

    ...... //①4.0新增了一個安裝包Verification功能,相關知識稍後分析

    //②執行pm命令,這個函式很有意思

    pm_command(transport,serial, argc, argv);

......

  cleanup_apk:

    //③在手機中執行shell rm 命令,刪除剛才傳送過去的目標APK檔案。為什麼要刪除呢

   delete_file(transport, serial, apk_dest);

    returnerr;

}

以上程式碼中共有三個關鍵點,分別是:

·  4.0新增了APK安裝過程中的Verification的功能。其實就是在安裝時,把相關資訊傳送給指定的Verification程式(另外一個APK),由它對要安裝的APK進行檢查(Verify)。這部分內容在後面分析APK 安裝時會介紹。目前,標準程式碼中還沒有從事Verification工作的APK。

·  呼叫pm_command進行安裝,這是一個比較有意思的函式,稍後對其進行分析。

·  安裝完後,執行shell rm刪除剛才傳送給手機的APK檔案。為什麼會刪除呢?因為PKMS在安裝過程中會將該APK複製一份到/data/app目錄下,所以/data/local/tmp下的對應檔案就可以刪除了。這部分程式碼在後面也能見到。

先來分析pm_command命令。為什麼說它很有意思呢?

4.4.2  pm分析

pm_command程式碼如下:

[-->commandline.c]

static int pm_command(transport_type transport,char* serial,

                      int argc, char** argv)

{

    charbuf[4096];

    snprintf(buf,sizeof(buf), "shell:pm");

  ......//準備引數

  //傳送"shell:pm install 引數"給手機端的adbd

   send_shellcommand(transport, serial, buf);

    return0;

}

手機端的adbd在收到客戶端發來的shellpm命令時會啟動一個shell,然後在其中執行pm。pm是什麼?為什麼可以在shell下執行?

提示讀者可以通過adb shell登入到自己手機,然後執行pm,看看會發現什麼。

pm實際上是一個指令碼,其內容如下:

[-->pm]

# Script to start "pm" on the device,which has a very rudimentary

# shell.

#

base=/system

export CLASSPATH=$base/frameworks/pm.jar

exec app_process $base/bincom.android.commands.pm.Pm "$@"

在編譯system.image時,Android.mk中會將該指令碼複製到system/bin目錄下。從pm指令碼的內容來看,它就是通過app_process執行pm.jar包的main函式。在卷I第4章分析Zygote時,已經介紹了app_process是一個Native程式,它通過建立虛擬機器啟動了Zygote,從而轉變為一個Java程式。實際上,app_process還可以通過類似的方法(即先建立Dalvik虛擬機器,然後執行某個類的main函式)來轉變成其他Java程式。

注意Android系統中常用的monkeytest、pm、am等(這些都是指令碼檔案)都是以這種方式啟動的,所以嚴格地說,app_process才是Android Java程式的老祖宗。

下面來分析pm.java,app_process執行的就是它定義的main函式,它相當於Java程式的入口函式,其程式碼如下:

[-->pm.java]

public static void main(String[] args) {

        newPm().run(args);//建立一個Pm物件,並執行它的run函式

}

  //直接分析run函式

public void run(String[] args) {

       boolean validCommand = false;

        ......

        //獲取PKMS的binder客戶端

        mPm= IPackageManager.Stub.asInterface(

                        ServiceManager.getService("package"));

        ......

       mArgs = args;

       String op = args[0];

       mNextArg = 1;

       ......//處理其他命令,這裡僅考慮install的處理

        if("install".equals(op)) {

           runInstall();

           return;

        }

   ......

}

接下來分析pm.java的runInstall函式,程式碼如下:

[-->pm.java]

private void runInstall() {

        intinstallFlags = 0;

       String installerPackageName = null;

 

       String opt;

       while ((opt=nextOption()) != null) {

           if (opt.equals("-l")) {

               installFlags |= PackageManager.INSTALL_FORWARD_LOCK;

            } else if (opt.equals("-r")) {

               installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;

           } else if (opt.equals("-i")) {

               installerPackageName = nextOptionData();

               ...... //引數解析

           } ......

        }

 

       final Uri apkURI;

       final Uri verificationURI;

       final String apkFilePath = nextArg();

       System.err.println("/tpkg: " + apkFilePath);

        if(apkFilePath != null) {

           apkURI = Uri.fromFile(new File(apkFilePath));

        }......

        //獲取Verification Package的檔案位置

       final String verificationFilePath = nextArg();

        if(verificationFilePath != null) {

          verificationURI = Uri.fromFile(new File(verificationFilePath));

        }else {

           verificationURI = null;

        }

        //建立PackageInstallObserver,用於接收PKMS的安裝結果

       PackageInstallObserver obs = new PackageInstallObserver();

        try{

          //①呼叫PKMS的installPackageWithVerification完成安裝

           mPm.installPackageWithVerification(apkURI, obs,

                                  installFlags,installerPackageName,

                                  verificationURI,null);

    synchronized (obs) {

      while(!obs.finished) {

          try{

                  obs.wait();//等待安裝結果

             } ......

         }

         if(obs.result == PackageManager.INSTALL_SUCCEEDED) {

             System.out.println("Success");//安裝成功,列印Success

         }......//安裝失敗,列印失敗原因

        } ......

    }

Pm解析引數後,最終通過PKMS的Binder客戶端呼叫installPackageWithVerification以完成後續的安裝工作,所以,下面進入PKMS看看安裝到底是怎麼一回事。

 

4.4.3  installPackageWithVerification函式分析

installPackageWithVerification的程式碼如下:

[-->PackageManagerService.java::installPackageWithVerification函式]

public void installPackageWithVerification(UripackageURI,

            IPackageInstallObserverobserver,

           int flags, String installerPackageName, Uri verificationURI,

           ManifestDigest manifestDigest) {

        //檢查客戶端程式是否具有安裝Package的許可權。在本例中,該客戶端程式是shell

       mContext.enforceCallingOrSelfPermission(

               android.Manifest.permission.INSTALL_PACKAGES,null);

       final int uid = Binder.getCallingUid();

       final int filteredFlags;

        if(uid == Process.SHELL_UID || uid == 0) {

             ......//如果通過shell pm的方式安裝,則增加INSTALL_FROM_ADB標誌

           filteredFlags = flags | PackageManager.INSTALL_FROM_ADB;

        }else {

           filteredFlags = flags & ~PackageManager.INSTALL_FROM_ADB;

        }

        //建立一個Message,code為INIT_COPY,將該訊息傳送給之前在PKMS建構函式中

       //建立的mHandler物件,將在另外一個工作執行緒中處理此訊息

       final Message msg = mHandler.obtainMessage(INIT_COPY);

        //建立一個InstallParams,其基類是HandlerParams

       msg.obj = new InstallParams(packageURI, observer,

                   filteredFlags,installerPackageName,

                  verificationURI,manifestDigest);

       mHandler.sendMessage(msg);

}

installPackageWithVerification函式倒是蠻清閒,簡簡單單建立幾個物件,然後傳送INIT_COPY訊息給mHandler,就甩手退出了。根據之前在PKMS建構函式中介紹的知識可知,mHandler被繫結到另外一個工作執行緒(藉助ThreadHandler物件的Looper)中,所以該INIT_COPY訊息也將在那個工作執行緒中進行處理。我們馬上轉戰到那。

1.  INIT_COPY處理

INIT_COPY只是安裝流程的第一步。先來看相關程式碼:

[-->PackageManagerService.java::handleMesssage]

public void handleMessage(Message msg) {

  try {

         doHandleMessage(msg);//呼叫doHandleMessage函式

       } ......

 }

 voiddoHandleMessage(Message msg) {

 switch(msg.what) {

    caseINIT_COPY: {

      //①這裡記錄的是params的基類型別HandlerParams,實際型別為InstallParams

     HandlerParams params = (HandlerParams) msg.obj;

      //idx為當前等待處理的安裝請求的個數

      intidx = mPendingInstalls.size();

      if(!mBound) {

        /*

        很多讀者可能想不到,APK的安裝居然需要使用另外一個APK提供的服務,該服務就是

         DefaultContainerService,由DefaultCotainerService.apk提供,

         下面的connectToService函式將呼叫bindService來啟動該服務

        */

        if(!connectToService()) {

             return;

         }else {//如果已經連上,則以idx為索引,將params儲存到mPendingInstalls中

           mPendingInstalls.add(idx, params);

          }

        } else {

           mPendingInstalls.add(idx, params);

            if(idx == 0) {

            //如果安裝請求佇列之前的狀態為空,則表明要啟動安裝

            mHandler.sendEmptyMessage(MCS_BOUND);

           }

         }

        break;

        }

      ......//後續再分析

這裡假設之前已經成功啟動了DefaultContainerService(以後簡稱DCS),並且idx為零,所以這是PKMS首次處理安裝請求,也就是說,下一個將要處理的是MCS_BOUND訊息。

注意connectToService在呼叫bindService時會傳遞一個DefaultContainerConnection型別的物件,以接收服務啟動的結果。當該服務成功啟動後,此物件的onServiceConnected被呼叫,其內部也將傳送MCS_BOUND訊息給mHandler。

2.  MCS_BOUND處理

現在,安裝請求的狀態從INIT_COPY變成MCS_BOUND了,此時的處理流程時怎樣的呢?依然在doHandleMessage函式中,直接從對應的case開始,程式碼如下:

[-->PackageManagerService.java::]

......//接doHandleMesage中的switch/case

case MCS_BOUND: {

  if(msg.obj != null) {

     mContainerService= (IMediaContainerService) msg.obj;

  }

  if(mContainerService == null) {

    ......//如果沒法啟動該service,則不能安裝程式

     mPendingInstalls.clear();

  } else if(mPendingInstalls.size() > 0) {

      HandlerParamsparams = mPendingInstalls.get(0);

      if(params != null) {

         //呼叫params物件的startCopy函式,該函式由基類HandlerParams定義

        if(params.startCopy()) {

           ......

            if(mPendingInstalls.size() > 0) {

               mPendingInstalls.remove(0);//刪除佇列頭

             }

          if (mPendingInstalls.size() == 0) {

            if (mBound) {

              ......//如果安裝請求都處理完了,則需要和Service斷絕聯絡,

              //通過傳送MSC_UNB訊息處理斷交請求。讀者可自行研究此情況的處理流程

               removeMessages(MCS_UNBIND);

               Message ubmsg = obtainMessage(MCS_UNBIND);

               sendMessageDelayed(ubmsg, 10000);

            }

            }else {

              //如果還有未處理的請求,則繼續傳送MCS_BOUND訊息。

              //為什麼不通過一個迴圈來處理所有請求呢

              mHandler.sendEmptyMessage(MCS_BOUND);

             }

            }

     } ......

    break;

MCS_BOUND的處理還算簡單,就是呼叫HandlerParams的startCopy函式。在深入分析前,應先認識一下HandlerParams及相關的物件。

(1) HandlerParams和InstallArgs介紹

除了HandlerParams家族外,這裡提前請出另外一個家族InstallArgs及其成員,如圖4-8所示。


圖4-8  HandlerParams及InstallArgs家族成員

由圖4-8可知:

·  HandlerParams和InstallArgs均為抽象類。

·  HandlerParams有三個子類,分別是InstallParams、MoveParams和MeasureParams。其中,InstallParams用於處理APK的安裝,MoveParams用於處理某個已安裝APK的搬家請求(例如從內部儲存移動到SD卡上),MeasureParams用於查詢某個已安裝的APK佔據儲存空間的大小(例如在設定程式中得到的某個APK使用的快取檔案的大小)。

·  對於InstallParams來說,它還有兩個伴兒,即InstallArgs的派生類FileInstallArgs和SdInstallArgs。其中,FileInstallArgs針對的是安裝在內部儲存的APK,而SdInstallArgs針對的是那些安裝在SD卡上的APK。

本節將討論用於內部儲存安裝的FileInstallArgs。

提示讀者可以在介紹完MountService後,結合本章知識點,自行研究SdInstallArgs的處理流程。

在前面MCS_BOUND的處理中,首先呼叫InstallParams的startCopy函式,該函式由其基類HandlerParams實現,程式碼如下:

[-->PackageManagerService.java::HandlerParams.startCopy函式]

final boolean startCopy() {

    booleanres;

    try {

    //MAX_RETIRES目前為4,表示嘗試4次安裝,如果還不成功,則認為安裝失敗

    if(++mRetries > MAX_RETRIES) {

        mHandler.sendEmptyMessage(MCS_GIVE_UP);

        handleServiceError();

        return false;

    } else {

         handleStartCopy();//①呼叫派生類的handleStartCopy函式

         res= true;

      }

    } ......

   handleReturnCode();//②呼叫派生類的handleReturnCode,返回處理結果

    returnres;

}

在上述程式碼中,基類的startCopy將呼叫子類實現的handleStartCopy和handleReturnCode函式。下面來看InstallParams是如何實現這兩個函式的。

(2) InstallParams分析

先來看派生類InstallParams的handleStartCopy函式,程式碼如下:

[-->PackageManagerService::InstallParams.handleStartCopy]

public void handleStartCopy() throwsRemoteException {

    int ret= PackageManager.INSTALL_SUCCEEDED;

    finalboolean fwdLocked = //本書不考慮fwdLocked的情況

        (flags &PackageManager.INSTALL_FORWARD_LOCK) != 0;

    //根據adb install的引數,判斷安裝位置

    finalboolean onSd = (flags & PackageManager.INSTALL_EXTERNAL) != 0;

    finalboolean onInt = (flags & PackageManager.INSTALL_INTERNAL) != 0;

   PackageInfoLite pkgLite = null;

    if(onInt && onSd) {

        //APK不能同時安裝在內部儲存和SD卡上

       ret =PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;

    } elseif (fwdLocked && onSd) {

    //fwdLocked的應用不能安裝在SD卡上

      ret =PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;

    } else {

       finallong lowThreshold;

       //獲取DeviceStorageMonitorService的binder客戶端

       finalDeviceStorageMonitorService dsm =                           

             (DeviceStorageMonitorService) ServiceManager.getService(

                                  DeviceStorageMonitorService.SERVICE);

       if(dsm == null) {

         lowThreshold = 0L;

       }else {

       //從DSMS查詢內部空間最小余量,預設是總空間的10%

     lowThreshold = dsm.getMemoryLowThreshold();

     }

     try {

        //授權DefContainerService URI讀許可權

      mContext.grantUriPermission(DEFAULT_CONTAINER_PACKAGE,

                  packageURI,Intent.FLAG_GRANT_READ_URI_PERMISSION);

       //①呼叫DCS的getMinimalPackageInfo函式,得到一個PackageLite物件

       pkgLite =mContainerService.getMinimalPackageInfo(packageURI,

                                   flags,lowThreshold);

     }finally ......//撤銷URI授權

    //PacakgeLite的recommendedInstallLocation成員變數儲存該APK推薦的安裝路徑

   int loc =pkgLite.recommendedInstallLocation;

   if (loc== PackageHelper.RECOMMEND_FAILED_INVALID_LOCATION) {

         ret= PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;

   } else if......{

   } else {

       //②根據DCS返回的安裝路徑,還需要呼叫installLocationPolicy進行檢查

       loc =installLocationPolicy(pkgLite, flags);

       if(!onSd && !onInt) {

          if(loc == PackageHelper.RECOMMEND_INSTALL_EXTERNAL) {

              flags |= PackageManager.INSTALL_EXTERNAL;

               flags &=~PackageManager.INSTALL_INTERNAL;

              } ......//處理安裝位置為內部儲存的情況

           }

       }

    }

  //③建立一個安裝引數物件,對於安裝位置為內部儲存的情況,args的真實型別為FileInstallArgs

    finalInstallArgs args = createInstallArgs(this);

    mArgs =args;

    if (ret== PackageManager.INSTALL_SUCCEEDED) {

        final int requiredUid = mRequiredVerifierPackage == null ? -1

                      :getPackageUid(mRequiredVerifierPackage);

        if(requiredUid != -1 && isVerificationEnabled()) {

            ......//④待會再討論verification的處理

         }else {

         //⑤呼叫args的copyApk函式

         ret= args.copyApk(mContainerService, true);

      }

   }

   mRet =ret;//確定返回值

}

在以上程式碼中,一共列出了五個關鍵點,總結如下:

·  呼叫DCS的getMinimalPackageInfo函式,將得到一個PackageLite物件,該物件是一個輕量級的用於描述APK的結構(相比PackageParser.Package來說)。在這段程式碼邏輯中,主要想取得其recommendedInstallLocation的值。此值表示該APK推薦的安裝路徑。

·  呼叫installLocationPolicy檢查推薦的安裝路徑。例如系統Package不允許安裝在SD卡上。

·  createInstallArgs將根據安裝位置建立不同的InstallArgs。如果是內部儲存,則返回FileInstallArgs,否則為SdInstallArgs。

·  在正式安裝前,應先對該APK進行必要的檢查。這部分程式碼後續再介紹。

·  呼叫InstallArgs的copyApk。對本例來說,將呼叫FileInstallArgs的copyApk函式。

下面圍繞這五個基本關鍵點展開分析,其中installLocationPolicy和createInstallArgs比較簡單,讀者可自行研究。

3.  handleStartCopy分析

(1) DefaultContainerService分析

首先分析DCS的getMinimalPackageInfo函式,其程式碼如下:

[-->DefaultContainerService.java::getMinimalPackageInfo函式]

public PackageInfoLite getMinimalPackageInfo(finalUri fileUri, int flags,

                                                      longthreshold) {

   //注意該函式的引數:fileUri指向該APK的檔案路徑(此時還在/data/local/tmp下)

  PackageInfoLite ret = new PackageInfoLite();

   ......

   Stringscheme = fileUri.getScheme();

   ......

   StringarchiveFilePath = fileUri.getPath();

  DisplayMetrics metrics = new DisplayMetrics();

  metrics.setToDefaults();

   //呼叫PackageParser的parsePackageLite解析該APK檔案

  PackageParser.PackageLite pkg =

         PackageParser.parsePackageLite(archiveFilePath,0);

   if (pkg== null) {//解析失敗

   ......//設定錯誤值

   returnret;

 }

  ret.packageName = pkg.packageName;

  ret.installLocation = pkg.installLocation;

  ret.verifiers = pkg.verifiers;

   //呼叫recommendAppInstallLocation,取得一個合理的安裝位置

  ret.recommendedInstallLocation =

          recommendAppInstallLocation(pkg.installLocation,archiveFilePath,

                                           flags, threshold);

   returnret;

}

APK可在AndroidManifest.xml中宣告一個安裝位置,不過DCS除了解析該位置外,還需要做進一步檢查,這個工作由recommendAppInstallLocation函式完成,程式碼如下:

[-->DefaultContainerService.java::recommendAppInstallLocation函式]

private int recommendAppInstallLocation(intinstallLocation,

                               StringarchiveFilePath, int flags,long threshold) {

 int prefer;

 booleancheckBoth = false;

 check_inner: {

   if((flags & PackageManager.INSTALL_FORWARD_LOCK) != 0) {

        prefer = PREFER_INTERNAL;

        break check_inner; //根據FOWRAD_LOCK的情況,只能安裝在內部儲存

    } elseif ((flags & PackageManager.INSTALL_INTERNAL) != 0) {

        prefer = PREFER_INTERNAL;

        break check_inner;

   }

   ......//檢查各種情況

  } else if(installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {

     prefer= PREFER_INTERNAL;//一般設定的位置為AUTO,預設是內部空間

    checkBoth = true; //設定checkBoth為true

     breakcheck_inner;

 }

  //查詢settings資料庫中的secure表,獲取使用者設定的安裝路徑

  intinstallPreference =

        Settings.System.getInt(getApplicationContext()

            .getContentResolver(),

             Settings.Secure.DEFAULT_INSTALL_LOCATION,

             PackageHelper.APP_INSTALL_AUTO);

   if(installPreference == PackageHelper.APP_INSTALL_INTERNAL) {

       prefer = PREFER_INTERNAL;

       break check_inner;

   } else if(installPreference == PackageHelper.APP_INSTALL_EXTERNAL) {

      prefer= PREFER_EXTERNAL;

      breakcheck_inner;

   }

   prefer =PREFER_INTERNAL;

  }

  //判斷外部儲存空間是否為模擬的,這部分內容我們以後再介紹

  finalboolean emulated = Environment.isExternalStorageEmulated();

  final FileapkFile = new File(archiveFilePath);

  booleanfitsOnInternal = false;

  if(checkBoth || prefer == PREFER_INTERNAL) {

      try {//檢查內部儲存空間是否足夠大

          fitsOnInternal = isUnderInternalThreshold(apkFile, threshold);

      } ......

   }

   booleanfitsOnSd = false;

   if(!emulated && (checkBoth || prefer == PREFER_EXTERNAL)) {

        try{ //檢查外部儲存空間是否足夠大

         fitsOnSd = isUnderExternalThreshold(apkFile);

       } ......

   }

  if (prefer== PREFER_INTERNAL) {

      if(fitsOnInternal) {//返回推薦安裝路徑為內部空間

        return PackageHelper.RECOMMEND_INSTALL_INTERNAL;

         }

    } elseif (!emulated && prefer == PREFER_EXTERNAL) {

      if(fitsOnSd) {//返回推薦安裝路徑為外部空間

         returnPackageHelper.RECOMMEND_INSTALL_EXTERNAL;

     }

 }

 

 if(checkBoth) {

     if(fitsOnInternal) {//如果內部儲存滿足條件,先返回內部空間

        return PackageHelper.RECOMMEND_INSTALL_INTERNAL;

       }else if (!emulated && fitsOnSd) {

         return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;

      }

  }

     ...... //到此,前幾個條件都不滿足,此處將根據情況返回一個明確的錯誤值

     returnPackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;

   }

}

DCS的getMinimalPackageInfo函式為了得到一個推薦的安裝路徑做了不少工作,其中,各種安裝策略交叉影響。這裡總結一下相關的知識點:

·  APK在AndroidManifest.xml中設定的安裝點預設為AUTO,在具體對應時傾向內部空間。

·  使用者在Settings資料庫中設定的安裝位置。

·  檢查外部儲存或內部儲存是否有足夠空間。

(2) InstallArgs的copyApk函式分析

至此,我們已經得到了一個合適的安裝位置(先略過Verification這一步)。下一步工作就由copyApk來完成。根據函式名可知該函式將完成APK檔案的複製工作,此中會有蹊蹺嗎?來看下面的程式碼。

[-->PackageManagerService.java::InstallArgs.copyApk函式]

int copyApk(IMediaContainerService imcs, booleantemp) throws RemoteException {

   if (temp){

  /*

    本例中temp引數為true,createCopyFile將在/data/app下建立一個臨時檔案。

    臨時檔名為vmdl-隨機數.tmp。為什麼會用這樣的檔名呢?

    因為PKMS通過Linux的inotify機制監控了/data/app,目錄,如果新複製生成的檔名字尾

    為apk,將觸發PKMS掃描。為了防止發生這種情況,這裡複製生成的檔案才有了

    如此奇怪的名字

  */

   createCopyFile();

  }

   FilecodeFile = new File(codeFileName);

   ......

  ParcelFileDescriptor out = null;

   try {

     out =ParcelFileDescriptor.open(codeFile,

                            ParcelFileDescriptor.MODE_READ_WRITE);

           }......

     int ret= PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;

     try {

       mContext.grantUriPermission(DEFAULT_CONTAINER_PACKAGE,

                   packageURI,Intent.FLAG_GRANT_READ_URI_PERMISSION);

        //呼叫DCS的copyResource,該函式將執行復制操作,最終結果是/data/local/tmp

       //下的APK檔案被複制到/data/app下,檔名也被換成vmdl-隨機數.tmp

         ret= imcs.copyResource(packageURI, out);

    }finally {

      ......//關閉out,撤銷URI授權

           }

    returnret;

 }

關於臨時檔案,這裡提供一個示例,如圖4-9所示。


圖4-9  createCopyFile生成的臨時檔案

由圖4-9可知:/data/app下有兩個檔案,第一個是正常的APK檔案,第二個是createCopyFile生成的臨時檔案。

4.  handleReturnCode分析

在HandlerParams的startCopy函式中,handleStartCopy執行完之後,將呼叫handleReturnCode開展後續工作,程式碼如下:

[-->PackageManagerService.java::InstallParams.HandleParams]

void handleReturnCode() {

       if(mArgs != null) {

         //呼叫processPendingInstall函式,mArgs指向之前建立的FileInstallArgs物件

        processPendingInstall(mArgs, mRet);

    }

}

[-->PackageManagerService.java::]

private void processPendingInstall(finalInstallArgs args,

                                  final intcurrentStatus) {

   //向mHandler中拋一個Runnable物件

  mHandler.post(new Runnable() {

       publicvoid run() {

        mHandler.removeCallbacks(this);

         //建立一個PackageInstalledInfo物件,

       PackageInstalledInfo res = new PackageInstalledInfo();

       res.returnCode = currentStatus;

       res.uid = -1;

       res.pkg = null;

       res.removedInfo = new PackageRemovedInfo();

        if(res.returnCode == PackageManager.INSTALL_SUCCEEDED) {

           //①呼叫FileInstallArgs的doPreInstall

           args.doPreInstall(res.returnCode);

           synchronized (mInstallLock) {

               //②呼叫installPackageLI進行安裝

               installPackageLI(args, true, res);

            }

          //③呼叫FileInstallArgs的doPostInstall

         args.doPostInstall(res.returnCode);

        }

       final boolean update = res.removedInfo.removedPackage != null;

       boolean doRestore = (!update&& res.pkg != null &&

                    res.pkg.applicationInfo.backupAgentName!= null);

       int token;//計算一個ID號

        if(mNextInstallToken < 0) mNextInstallToken = 1;

            token = mNextInstallToken++;

            //建立一個PostInstallData物件

            PostInstallData data = new PostInstallData(args, res);

           //儲存到mRunningInstalls結構中,以token為key

           mRunningInstalls.put(token, data);

           if (res.returnCode ==PackageManager.INSTALL_SUCCEEDED && doRestore)

             {

                  ......//備份恢復的情況暫時不考慮

            }

           if(!doRestore) {

            //④拋一個POST_INSTALL訊息給mHandler進行處理

            Message msg = mHandler.obtainMessage(POST_INSTALL, token, 0);

            mHandler.sendMessage(msg);

           }

        }

        });

}

由上面程式碼可知,handleReturnCode主要做了4件事情:

·  呼叫InstallArgs的doPreInstall函式,在本例中是FileInstallArgs的doPreInstall函式。

·  呼叫PKMS的installPackageLI函式進行APK安裝,該函式內部將呼叫InstallArgs的doRename對臨時檔案進行改名。另外,還需要掃描此APK檔案。此過程和之前介紹的“掃描系統Package”一節的內容類似。至此,該APK中的私有財產就全部被登記到PKMS內部進行儲存了。

·  呼叫InstallArgs的doPostInstall函式,在本例中是FileInstallArgs的doPostInstall函式。

·  此時,該APK已經安裝完成(不論失敗還是成功),繼續向mHandler拋送一個POST_INSTALL訊息,該訊息攜帶一個token,通過它可從mRunningInstalls陣列中取得一個PostInstallData物件。

提示對於FileInstallArgs來說,其doPreInstall和doPostInstall都比較簡單,讀者可自行閱讀相關程式碼。另外,讀者也可自行研究PKMS的installPackageLI函式。

這裡介紹一下FileInstallArgs的doRename函式,它的功能是將臨時檔案改名,最終的檔案的名稱一般為“包名-數字.apk”。其中,數字是一個index,從1開始。讀者可參考圖4-9中/data/app目錄下第一個檔案的檔名。

5.  POST_INSTALL處理

現在需要處理POST_INSTALL訊息,因為adb install還等著安裝結果呢。相關程式碼如下:

[-->PackageManagerService.java::doHandleMessage函式]

......//接前面的switch/case

case POST_INSTALL: {

  PostInstallData data = mRunningInstalls.get(msg.arg1);

 mRunningInstalls.delete(msg.arg1);

  booleandeleteOld = false;

 

  if (data!= null) {

      InstallArgs args = data.args;

      PackageInstalledInfo res = data.res;

       if(res.returnCode == PackageManager.INSTALL_SUCCEEDED) {

           res.removedInfo.sendBroadcast(false, true);

           Bundle extras = new Bundle(1);

           extras.putInt(Intent.EXTRA_UID, res.uid);

           final boolean update = res.removedInfo.removedPackage != null;

           if (update) {

                extras.putBoolean(Intent.EXTRA_REPLACING, true);

           }

           //傳送PACKAGE_ADDED廣播

           sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,

                    res.pkg.applicationInfo.packageName,extras, null, null);

           if (update) {

           /*

           如果是APK升級,那麼傳送PACKAGE_REPLACE和MY_PACKAGE_REPLACED廣播。

          二者不同之處在於PACKAGE_REPLACE將攜帶一個extra資訊

         */

       }

      Runtime.getRuntime().gc();

       if(deleteOld) {

         synchronized (mInstallLock) {

           //呼叫FileInstallArgs的doPostDeleteLI進行資源清理

           res.removedInfo.args.doPostDeleteLI(true);

            }

        }

      if(args.observer != null) {

      try {

          // 向pm通知安裝的結果

        args.observer.packageInstalled(res.name, res.returnCode);

      } ......

} break;

 

4.4.4  APK 安裝流程總結

沒想到APK的安裝流程竟然如此複雜,其目的無非是讓APK中的私人財產公有化。相比之下,在PKMS建構函式中進行公有化改造就非常簡單。另外,如果考慮安裝到SD卡的處理流程,那麼APK的安裝將會更加複雜。

這裡要總結APK安裝過程中的幾個重要步驟,如圖4-10所示。


圖4-10  APK安裝流程

圖4-10中列出以下內容:

·  安裝APK到內部儲存空間這一工作流程涉及的主要物件包括:PKMS、DCS、InstallParams和FileInstallArgs。

·  此工作流程中每個物件涉及到的關鍵函式。

·  物件之間的呼叫通過虛線表達,呼叫順序通過①②③等標明。

4.4.5  Verification介紹

Verification功能的出現將打亂圖4-10的工作流程,所以這部分內容要放在最後來介紹。其程式碼在InstallParams的handleStartCopy中,如下所示:

[-->PackageManagerService.java::InstallParams.handleStartCopy函式]

  ......//此處已經獲得了合適的安裝位置

  finalInstallArgs args = createInstallArgs(this);

  mArgs =args;

if (ret == PackageManager.INSTALL_SUCCEEDED) {

    final int requiredUid =mRequiredVerifierPackage == null ? -1

                        :getPackageUid(mRequiredVerifierPackage);

  if (requiredUid != -1 &&isVerificationEnabled()) {

     //建立一個Intent,用於查詢滿足條件的廣播接收者

   finalIntent verification = new

                    Intent(Intent.ACTION_PACKAGE_NEEDS_VERIFICATION);

   verification.setDataAndType(packageURI, PACKAGE_MIME_TYPE);

   verification.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

    //查詢滿足Intent條件的廣播接收者

    finalList<ResolveInfo> receivers = queryIntentReceivers(

               verification,null,PackageManager.GET_DISABLED_COMPONENTS);

    // verificationId為當前等待Verification的安裝包個數

    finalint verificationId = mPendingVerificationToken++;

   //設定Intent的引數,例如要校驗的包名

    verification.putExtra(PackageManager.EXTRA_VERIFICATION_ID,

                              VerificationId);

    verification.putExtra(

                            PackageManager.EXTRA_VERIFICATION_INSTALLER_PACKAGE,

                            installerPackageName);

  verification.putExtra(

                   PackageManager.EXTRA_VERIFICATION_INSTALL_FLAGS,flags);

   if(verificationURI != null) {

     verification.putExtra(PackageManager.EXTRA_VERIFICATION_URI,

                                verificationURI);

    }

  finalPackageVerificationState verificationState = new

                       PackageVerificationState(requiredUid,args);

  //將上面建立的PackageVerificationState儲存到mPendingVerification中

 mPendingVerification.append(verificationId, verificationState);

  //篩選符合條件的廣播接收者

  finalList<ComponentName> sufficientVerifiers =

                   matchVerifiers(pkgLite,receivers,verificationState);

  if (sufficientVerifiers != null) {

      finalint N = sufficientVerifiers.size();

      ......

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

       finalComponentName verifierComponent = sufficientVerifiers.get(i);

       final Intent sufficientIntent = newIntent(verification);

       sufficientIntent.setComponent(verifierComponent);

       //向校驗包傳送廣播

       mContext.sendBroadcast(sufficientIntent);

       }

    }

 }

   //除此之外,如果在執行adb install的時候指定了校驗包,則需要向其單獨傳送校驗廣播

   finalComponentName requiredVerifierComponent =

                        matchComponentForVerifier(mRequiredVerifierPackage,

                       receivers);

   if (ret == PackageManager.INSTALL_SUCCEEDED

        &&mRequiredVerifierPackage != null) {

      verification.setComponent(requiredVerifierComponent);

      mContext.sendOrderedBroadcast(verification,

      android.Manifest.permission.PACKAGE_VERIFICATION_AGENT,

           new BroadcastReceiver() {

           //呼叫sendOrderdBroadcast,並傳遞一個BroadcastReceiver,該物件將在

          //廣播傳送的最後被呼叫。讀者可參考sendOrderdBroadcast的文件說明

           public void onReceive(Context context, Intent intent) {

            final Message msg =mHandler.obtainMessage(

                      CHECK_PENDING_VERIFICATION);

            msg.arg1 = verificationId;

            //設定一個超時執行時間,該值來自Settings資料庫的secure表,預設為60秒

            mHandler.sendMessageDelayed(msg, getVerificationTimeout());

           }

         },null, 0, null, null);

           mArgs = null;

        }

    }......//不用做Verification的流程

PKMS的Verification工作其實就是收集安裝包的資訊,然後向對應的校驗者傳送廣播。但遺憾的是,當前Android中還沒有能處理Verification的元件。

另外,該元件處理完Verification後,需要呼叫PKMS的verifyPendingInstall函式,以通知校驗結果。

4.5  queryIntentActivities分析

PKMS除了負責Android系統中Package的安裝、升級、解除安裝外,還有一項很重要的職責,就是對外提供統一的資訊查詢功能,其中包括查詢系統中匹配某Intent的Activities、BroadCastReceivers或Services等。本節將以查詢匹配某Intent的Activities為例,介紹PKMS在這方便提供的服務。

正式分析queryIntentActivities之前,先來認識一下Intent及IntentFilter。

4.5.1  Intent及IntentFilter介紹

1.  Intent介紹

Intent中文是“意圖”的意思,它是Android系統中一個很重要的概念,其基本思想來源於對日常生活及行為的高度抽象。我們結合用人單位招聘的例子介紹Intent背後的思想。

·  假設某用人單位現需招聘人員完成某項工作。該單位首先其需求發給獵頭公司。

·  獵頭公司從其內部的資訊庫中查詢合適的人選。獵頭公司除了考慮用人單位的需求外,還需要考慮求職者本身的要求,例如有些求職者對工作地點、加班等有要求。

·  二者匹配後,就會得到滿足要求的求職者。之後用人單位將工作交給滿足條件的人員來完成。

在現實生活中,用人單位還需和求職者進行一系列其他互動工作,例如面試、簽訂合同之類。但是從完成工作的角度來看,只要把工作任務交給滿足要求的求職者去做即可,中間的系列行為和工作任務本身沒有太大關係。因此,Android並未將這部分內容抽象化。

意圖,是一個非常抽象的概念,在編碼設計中,如何將它例項化呢?Android系統明確指定的一個Intent可由兩方面屬性來衡量。

·  主要屬性:包括Action和Data。其中Action用於表示該Intent所表達的動作意圖、Data用於表示該Action所操作的資料。

·  次要屬性:包括Category、Type、Component和Extras。其中Category表示類別,Type表示資料的MIME型別,Component可用於指定特定的Intent響應者(例如指定廣播接收者為某Package的某個BroadcastReceiver),Extras用於承載其他的資訊。

如果Intent是一份用工需求表,那麼上述資訊就是該表的全部可填項。在實際使用中,可根據需要填寫該表的內容。

當這份需求表傳給獵頭公司後,獵頭公司就根據該表所填寫的內容,進一步對Intent進行分類。

·  Explicit Intents:這類Intent明確指明瞭要找哪些人。在程式碼中通過setComponent或setClass來鎖定目標物件。處理這種Intent,工作就很輕鬆了。

·  Implicit Intents:這一類Intents只標明瞭工作內容,而沒有指定具體人名。對於這類意圖,獵頭公司不得不做一系列複雜的工作才能找到滿足用人單位需求的人才。

Intent就先介紹到這裡。下面來看在這次招聘過程中求職者填寫的資訊。

2.  IntentFilter介紹

求職方需要填寫IntentFilter來表達自己的訴求。Andorid規定了3項內容.

·  Action:求職方支援的Intent動作(和Intent中的Action對應)。

·  Category:求職方支援的Intent種類(和Intent的Category對應)。

·  Data:求職方支援的Intent 資料(和Intent的Data對應,包括URI和MIME型別)。

至此,獵頭公司既有了需求,又有了求職者的資訊,馬上要做的工作就是匹配查詢。在Android中,該工作被稱為Intent Resolution。由於現在及未來人才都是最寶貴的資源,因此獵頭公司在做匹配工作時,將以Intent Filter列出的3項內容為參考標準,具體步驟如下:

·  首先匹配IntentFilter的Action,如果Intent設定的Action不滿足IntentFilter的Action,則匹配失敗。如果IntentFilter未設定Action,則匹配成功。

·  然後檢查IntentFilter的Category,匹配方法同Action的匹配,唯一有些例外的是Category為CATEGORY_DEFAULT的情況。

·  最後檢查Data。Data的匹配過程比較繁瑣,因為它和IntentFilter設定的Data內容有關,見接下來的介紹。

IntentFilter中的Data可以包括兩個內容。

·  URI:完整格式為“scheme://host:port/path”,包含4個部分,scheme、host、port和path。其中host和port合起來標示URI authority,用於指明伺服器的網路地址(IP加埠號)。由於URI最多可包含,4個部分,因此要根據情況相應部分做匹配檢查。

·  Date type:指定資料的MIME型別

要特別注意的是,URI中也可以攜帶資料的型別資訊,所以在匹配過程中,還需要考慮URI中指定的資料型別。

提示關於具體的匹配流程,請讀者務必閱讀SDK docs/guide/topics/intents/intents-filters.html中的說明。

4.5.2  Activity資訊的管理

前面在介紹PKMS掃描APK時提到,PKMS將解析得到的Package私有的Activity資訊加入到自己的資料結構mActivities中儲存。先來回顧一下程式碼:

[-->PacakgeManagerService.java::scanPackageLI函式]

     ......//此時APK檔案已經解析完成

     N =pkg.activities.size();//取出該APK中包含的Activities資訊

     r =null;

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

        PackageParser.Activity a = pkg.activities.get(i);

        a.info.processName = fixProcessName(pkg.applicationInfo.processName,

                                     a.info.processName,pkg.applicationInfo.uid);

         mActivities.addActivity(a,"activity");//①加到mActivities中儲存

 }

上面的程式碼中有兩個比較重要的資料結構,如圖4-11所示。


圖4-11  相關資料結構示意圖

結合程式碼,由圖4-11可知:

·  mActivities為ActivityIntentResolver型別,是PKMS的成員變數,用於儲存系統中所有與Activity相關的資訊。此資料結構內部有一個mActivities變數,它以ComponetName為Key,儲存PackageParser.Activity物件

·  從APK中解析得到的所有和Activity相關的資訊(包括在XML中宣告的IntentFilter標籤)都由PacakgeParser.Activity來儲存。

前面程式碼中呼叫addActivity函式完成了私有資訊的公有化。addActivity函式的程式碼如下:

[-->PacakgeManagerService.java::ActivityIntentResolver.addActivity]

public final voidaddActivity(PackageParser.Activity a, String type) {

     finalboolean systemApp = isSystemApp(a.info.applicationInfo);

     //將Component和Activity儲存到mActivities中

    mActivities.put(a.getComponentName(), a);

     finalint NI = a.intents.size();

     for(int j=0; j<NI; j++) {

     //ActivityIntentInfo儲存的就是XML中宣告的IntentFilter資訊

    PackageParser.ActivityIntentInfo intent = a.intents.get(j);

     if(!systemApp && intent.getPriority() > 0 &&"activity".equals(type)) {

          //非系統APK的priority必須為0。後續分析中將介紹priority的作用

          intent.setPriority(0);

        }

         addFilter(intent);//接下來將分析這個函式

      }

 }

下面來分析addFilter函式,這裡涉及較多的複雜資料結構,程式碼如下:

[-->IntentResolver.java::IntentResolver.addFilter]

public void addFilter(F f) {

        ......

       mFilters.add(f);//mFilters儲存所有IntentFilter資訊

       //除此之外,為了加快匹配工作的速度,還需要分類儲存IntentFilter資訊

       //下邊register_xxx函式的最後一個引數用於列印資訊

        intnumS = register_intent_filter(f, f.schemesIterator(),

                         mSchemeToFilter,"      Scheme: ");

        intnumT = register_mime_types(f, "     Type: ");

        if(numS == 0 && numT == 0) {

           register_intent_filter(f, f.actionsIterator(),

                           mActionToFilter,"      Action: ");

        }

        if(numT != 0) {

           register_intent_filter(f, f.actionsIterator(),

                   mTypedActionToFilter, "     TypedAction: ");

        }

 }

正如程式碼註釋中所說,為了加快匹配工作的速度,這裡使用了泛型程式設計並定義了較多的成員變數。下面總結一下這些變數的作用(注意,除mFilters為HashSet<F>型別外,其他成員變數的型別都是HashMap<String, ArrayList<F>>,其中F為模板引數)。

·  mSchemeToFilter:用於儲存URI中與schema相關的IntentFilter資訊。

·  mActionToFilter:用於儲存僅設定Action條件的IntentFilter資訊。

·  mTypedActionToFilter:用於儲存既設定了Action又設定了Data的MIME型別的IntentFilter資訊。

·  mFilters:用於儲存所有IntentFilter資訊

·  mWildTypeToFilter:用於儲存設定了Data型別類似“image/*”的IntentFilter,但是設定MIME型別類似“Image/jpeg”的不算在此類。

·  mTypeToFilter:除了包含mWildTypeToFilter外,還包含那些指明瞭Data型別為確定引數的IntentFilter資訊,例如“image/*”和”image/jpeg“等都包含在mTypeToFilter中。

·  mBaseTypeToFilter:包含MIME中Base 型別的IntentFilter資訊,但不包括Sub type為“*”的IntentFilter。

不妨舉個例子來說明這些變數的用法。

假設,在XML中宣告一個IntentFilter,程式碼如下:

<intent-filter android:label="test">

   <actionandroid:name="android.intent.action.VIEW" />

   dataandroid:mimeType="audio/*" android:scheme="http"

 </intent-filter>

那麼:

·  在mTypedActionToFilter中能夠以“android.intent.action.VIEW”為key找到該IntentFilter。

·  在mWildTypeToFilter和mTypeToFilter中能夠以“audio”為key找到該IntentFilter。

·  在mSchemeToFilter中能夠以”http“為key找到該IntentFilter。

下面來分析Intent匹配查詢工作。

4.5.2  Intent 匹配查詢分析

1.  客戶端查詢

客戶端通過ApplicationPackageManager輸出的queryIntentActivities函式向PKMS發起一次查詢請求,程式碼如下:

[-->ApplicationPackageManager.java::queryIntentActivities]

public List<ResolveInfo>queryIntentActivities(Intent intent, int flags) {

   try {

           return mPM.queryIntentActivities(

               intent,//下面這句話很重要

               intent.resolveTypeIfNeeded(mContext.getContentResolver()),

               flags);

        }......

}

如果Intent的Data包含一個URI,那麼就需要查詢該URI的提供者(即ContentProvider)以取得該資料的資料型別。讀者可自行閱讀resolveTypeIfNeeded函式的程式碼。

另外,flags引數目前有3個可選值,分別是MATCH_DEFAULT_ONLY、GET_INTENT_FILTERS和GET_RESOLVED_FILTER。詳細資訊讀者可查詢SDK相關文件。

下面來看PKMS對匹配查詢的處理。

2.  queryIntentActivities分析

該函式程式碼如下:

[-->PacakgeManagerService.java::queryIntentActivities]

public List<ResolveInfo>queryIntentActivities(Intent intent,

                                  String resolvedType, int flags) {

       final ComponentName comp = intent.getComponent();

        if(comp != null) {

            //Explicit的Intents,直接根據component得到對應的ActivityInfo

           final List<ResolveInfo> list = new ArrayList<ResolveInfo>(1);

           final ActivityInfo ai = getActivityInfo(comp, flags);

           if (ai != null) {

               final ResolveInfo ri = new ResolveInfo();

               //ResovlerInfo的activityInfo指向查詢得到的ActivityInfo

               ri.activityInfo = ai;

               list.add(ri);

           }

           return list;

        }

 

       synchronized (mPackages) {

           final String pkgName = intent.getPackage();

           if (pkgName == null) {

                        //Implicit Intents,我們重點分析此中情況

               return mActivities.queryIntent(intent, resolvedType, flags);

           }

            //Intent指明瞭PackageName,比Explicit Intents情況差一點

           final PackageParser.Package pkg = mPackages.get(pkgName);

           if (pkg != null) {

               //其實是從該Package包含的Activities中進行匹配查詢

               return mActivities.queryIntentForPackage(intent, resolvedType,

                                           flags, pkg.activities);

           }

           return new ArrayList<ResolveInfo>();

        }

    }

上邊程式碼分三種情況:

·  如果Intent指明瞭Component,則直接查詢該Component對應的ActivityInfo。

·  如果Intent指明瞭Package名,則根據Package名找到該Package,然後再從該Package包含的Activities中進行匹配查詢。

·  如果上面條件都不滿足,則需要在全系統範圍內進行匹配查詢,這就是queryIntent的工作。

queryIntent函式的程式碼如下:

public List<ResolveInfo> queryIntent(Intentintent, String resolvedType,

                                            intflags) {

    mFlags =flags;

   //呼叫基類的queryIntent函式

   returnsuper.queryIntent(intent, resolvedType,

               (flags&PackageManager.MATCH_DEFAULT_ONLY) != 0);

 }

[-->IntentResolver.java::queryIntent]

public List<R> queryIntent(Intent intent,String resolvedType,

                             booleandefaultOnly) {

  Stringscheme = intent.getScheme();

 ArrayList<R> finalList = new ArrayList<R>();

 //最多有四輪匹配工作要做

 ArrayList<F> firstTypeCut = null;

 ArrayList<F> secondTypeCut = null;

 ArrayList<F> thirdTypeCut = null;

 ArrayList<F> schemeCut = null;

 //下面將設定各輪校驗者

 if(resolvedType != null) {

     intslashpos = resolvedType.indexOf('/');

      if(slashpos > 0) {

         final String baseType = resolvedType.substring(0, slashpos);

          if (!baseType.equals("*")) {

               if (resolvedType.length() != slashpos+2

                   || resolvedType.charAt(slashpos+1) != '*') {

                       firstTypeCut =mTypeToFilter.get(resolvedType);

                       secondTypeCut =mWildTypeToFilter.get(baseType);

                   }......//略去一部分內容

           }

        }

 

      if(scheme != null) {

           schemeCut = mSchemeToFilter.get(scheme);

        }

      if(resolvedType == null && scheme == null && intent.getAction()!= null)

      {

        //看來action的filter優先順序最低

       firstTypeCut = mActionToFilter.get(intent.getAction());

      }

   //FastImmutableArraySet是一種特殊的資料結構,用於儲存該Intent中攜帶的

   //Category相關的資訊。

   FastImmutableArraySet<String>categories = getFastIntentCategories(intent);

    if(firstTypeCut != null) {

     //匹配查詢,第一輪過關斬將

     buildResolveList(intent, categories, debug, defaultOnly,

             resolvedType, scheme, firstTypeCut,finalList);

    }

    if(secondTypeCut != null) {

        buildResolveList(intent, categories, debug, defaultOnly,

                   resolvedType, scheme, secondTypeCut, finalList);

     }

    if(thirdTypeCut != null) {

        buildResolveList(intent, categories, debug, defaultOnly,

                   resolvedType, scheme, thirdTypeCut, finalList);

     }

    if(schemeCut != null) {

        //生成符合schemeCut條件的finalList

        buildResolveList(intent, categories, debug, defaultOnly,

                   resolvedType, scheme, schemeCut, finalList);

    }

   //將匹配結果按Priority的大小排序

   sortResults(finalList);

    returnfinalList;

}

在以上程式碼中設定了最多四輪匹配關卡,然後逐一執行匹配工作。具體的匹配程式碼由buildResolveList完成,無非是一項查詢工作而已。此處就不再深究細節了,建議讀者在研究程式碼時以目的為導向,不宜深究其中的資料結構。

4.5.3 queryIntentActivities總結

本節分析了queryIntentActivities函式的實現,其功能很簡單,就是進行Intent匹配查詢。一路走來,相信讀者也感覺到旅程並不輕鬆,主要原因是涉及的資料結構較多,讓人有些頭暈。這裡,再次建議讀者不要在資料結構上花太多時間,最好結合SDK中的文件說明來分析相關程式碼。

4.6  installd及UserManager介紹

4.6.1  installd介紹

在前面對PKMS建構函式分析時介紹過一個Installer型別的物件mInstaller,它通過socket和後臺服務installd互動,以完成一些重要操作。這裡先回顧一下PKMS中mInstaller的呼叫方法:

mInstaller = new Installer();//建立一個Installer物件

//對某個APK檔案進行dexopt優化

mInstaller.dexopt(paths[i], Process.SYSTEM_UID,true);

//掃描完系統Package後,呼叫moveFiles函式

mInstaller.moveFiles();

//當儲存空間不足時,呼叫該函式清理儲存空間

mInstaller.freeCache(freeStorageSize);

Installer的種種行為都和其背後的installd有關。下面來分析installd。

1.  installd概貌

installd是一個native程式,程式碼非常簡單,其功能就是啟動一個socket,然後處理來自Installer的命令,其程式碼如下:

[-->installd.c]

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

    charbuf[BUFFER_MAX];

    structsockaddr addr;

   socklen_t alen;

    intlsocket, s, count;

    //

  if (初始化全域性變數,如果失敗則退出) {

    initialize_globals();

    initialize_directories();

        ......

    }

    ......

    lsocket= android_get_control_socket(SOCKET_PATH);

  

   listen(lsocket, 5);

   fcntl(lsocket, F_SETFD, FD_CLOEXEC);

 

    for (;;){

        alen= sizeof(addr);

        s =accept(lsocket, &addr, &alen);

       fcntl(s, F_SETFD, FD_CLOEXEC);

        for(;;) {

           unsigned short count;

           readx(s, &count, sizeof(count));

           //執行installer發出的命令,具體解釋見下文

            execute(s, buf);

        }

       close(s);

    }

 

    return0;

}

installd支援的命令及引數資訊都儲存在資料結構cmds中,程式碼如下:

[-->installd.c]

struct cmdinfo cmds[] = {//第二個變數是引數個數,第三個引數是命令響應函式

    {"ping",                 0,do_ping },

    {"install",              3,do_install },

    {"dexopt",               3,do_dexopt },

    {"movedex",              2,do_move_dex },

    {"rmdex",                1,do_rm_dex },

    {"remove",               2,do_remove },

    {"rename",               2, do_rename },

    {"freecache",            1,do_free_cache },

    {"rmcache",              1,do_rm_cache },

    {"protect",              2,do_protect },

    {"getsize",              4,do_get_size },

    {"rmuserdata",           2,do_rm_user_data },

    {"movefiles",            0,do_movefiles },

    {"linklib",              2,do_linklib },

    {"unlinklib",            1,do_unlinklib },

    {"mkuserdata",           3,do_mk_user_data },

    {"rmuser",               1,do_rm_user },

};

下面來分析相關的幾個命令。

2.  dexOpt命令分析

PKMS在需要對一個APK或jar包做dex優化時,會傳送dexopt命令給installd,相應的處理函式為do_dexopt,程式碼如下:

[-->installd.c]

static int do_dexopt(char **arg, charreply[REPLY_MAX])

{

     returndexopt(arg[0], atoi(arg[1]), atoi(arg[2]));

}

[-->commands.c]

int dexopt(const char *apk_path, uid_t uid, intis_public)

{

    structutimbuf ut;

    structstat apk_stat, dex_stat;

    chardex_path[PKG_PATH_MAX];

    chardexopt_flags[PROPERTY_VALUE_MAX];

    char*end;

    int res,zip_fd=-1, odex_fd=-1;

    ......

    //取出系統級的dexopt_flags引數

   property_get("dalvik.vm.dexopt-flags", dexopt_flags,"");

 

   strcpy(dex_path, apk_path);

    end =strrchr(dex_path, '.');

    if (end!= NULL) {

       strcpy(end, ".odex");

        if(stat(dex_path, &dex_stat) == 0) {

           return 0;

        }

    }

    //得到一個字串,用於描述dex檔名,位於/data/dalvik-cache/下

    if(create_cache_path(dex_path, apk_path)) {

       return -1;

    }

 

   memset(&apk_stat, 0, sizeof(apk_stat));

   stat(apk_path, &apk_stat);

 

    zip_fd =open(apk_path, O_RDONLY, 0);

    ......

   unlink(dex_path);

    odex_fd= open(dex_path, O_RDWR | O_CREAT | O_EXCL, 0644);

    ......

    pid_tpid;

    pid =fork();

    if (pid== 0) {

        ......//uid設定

        //建立一個新程式,然後對exec dexopt程式進行dex優化

        run_dexopt(zip_fd,odex_fd, apk_path, dexopt_flags);

       exit(67);  

} else {

        //installd將等待dexopt完成優化工作

        res= wait_dexopt(pid, apk_path);

        ......

    }

    ......//資源清理

    return-1;

}

讓人大跌眼鏡的是,dex優化工作竟然由installd委派給dexopt程式來實現。dex優化後會生成一個dex檔案,一般位於/data/dalvik-cache/目錄中。這裡給出一個示例,如圖4-12所示。


圖4-12  dex檔案示例

提示 dexopt程式由android原始碼/dalvik/dexopt/OptMain.cpp定義。感興趣的讀者可深入研究dex優化的工作原理。

3.  movefiles命令分析

PKMS掃描完系統Package後,將傳送該命令給installd,相應處理函式的程式碼如下:

[-->installd.c]

static int do_movefiles(char **arg, charreply[REPLY_MAX])

{

    returnmovefiles();

}

[-->commands.c]

int movefiles()

{

    DIR *d;

    int dfd,subfd;

    structdirent *de;

    structstat s;

    charbuf[PKG_PATH_MAX+1];

    intbufp, bufe, bufi, readlen;

 

    charsrcpkg[PKG_NAME_MAX];

    chardstpkg[PKG_NAME_MAX];

    charsrcpath[PKG_PATH_MAX];

    chardstpath[PKG_PATH_MAX];

    intdstuid=-1, dstgid=-1;

    inthasspace;

    //開啟/system/etc/updatecmds/目錄

    d =opendir(UPDATE_COMMANDS_DIR_PREFIX);

    if (d ==NULL) {

        gotodone;

    }

    dfd =dirfd(d);

       while((de = readdir(d))) {

        ......//解析該目錄下的檔案,然後執行對應操作

   }

   closedir(d);

done:

    return0;

}

先來看/system/etc/updatecmds/目錄下到底是什麼檔案,這裡給出一個示例,如圖4-13所示。


圖4-13  movefiles示例

以圖4-13中最後兩行為例,movefiles將把com.google.android.gsf下的databases目錄轉移到com.andorid.providers.im下。從檔案中的註釋可知,movefiles的功能和系統升級有關。

4.  doFreeCache

第3章介紹了DeviceStorageMonitorService,當系統空間不夠時,DSMS會呼叫PKMS的freeStorageAndNotify函式進行空間清理。該工作真正的實施者是installd,相應的處理命令為do_free_cache,其程式碼如下:

[-->installd.c]

static int do_free_cache(char **arg, charreply[REPLY_MAX])

{

    returnfree_cache((int64_t)atoll(arg[0]));

}

[-->commands.c]

int free_cache(int64_t free_size)

{

    constchar *name;

    int dfd,subfd;

    DIR *d;

    structdirent *de;

    int64_tavail;

 

    avail =disk_free();//獲取當前系統的剩餘空間大小

    if(avail < 0) return -1;

    if(avail >= free_size) return 0;

    d =opendir(android_data_dir.path);//開啟/data/目錄

    dfd =dirfd(d);

    while((de = readdir(d))) {

        if (de->d_type != DT_DIR) continue;

        name= de->d_name;

       ......//略過.和..檔案

       subfd = openat(dfd, name, O_RDONLY | O_DIRECTORY);

        //刪除/data及各級子目錄中的cache資料夾

       delete_dir_contents_fd(subfd, "cache");

       close(subfd);

       ......//如果剩餘空間恢復正常,則返回

    }

   closedir(d);

    return-1;//清理空間後,仍然不滿足要求

}

installd的介紹就到此為止,這部分內容比較簡單,讀者完全可自行深入研究。

4.6.2  UserManager介紹

UserManager是Andorid 4.0新增的一個功能,其作用是管理手機上的不同使用者。這一點和PC上的Windows系統比較相似,例如,在Windows上安裝程式時,都會提示是安裝給本人使用還是安裝給系統所有使用者使用。非常遺憾的是,在目前的Andorid版本中,該功能尚未完全實現,在SDK中也沒有相關說明。不過從現有程式碼中,也能發現一些蛛絲馬跡。

提示 小米手機的訪客模式和UserManager比較相似。

1.  UserManager建構函式分析

在PKMS中,建立UserManager呼叫的程式碼如下:

//mUserAppDataDir指向/data/app。該目錄中包含的是非系統APK檔案

mUserManager = new UserManager(mInstaller,mUserAppDataDir);

[-->UserManager.java]

public UserManager(Installer installer, FilebaseUserPath) {

   this(Environment.getDataDirectory(), baseUserPath);

   mInstaller = installer;

}

UserManager(File dataDir, File baseUserPath) {

   //mUsersDir指向/data/system/users目錄

   mUsersDir = new File(dataDir, USER_INFO_DIR);

   mUsersDir.mkdirs();//建立該目錄

   mBaseUserPath = baseUserPath;

   FileUtils.setPermissions(mUsersDir.toString(),

            FileUtils.S_IRWXU|FileUtils.S_IRWXG

           |FileUtils.S_IROTH|FileUtils.S_IXOTH,

               -1, -1);

   //mUserListFile指向/data/system/user/userlist.xml

   mUserListFile = new File(mUsersDir, USER_LIST_FILENAME);

   readUserList();//解析userlist.xml檔案

}

此處不深入readUserList程式碼了,只介紹其內部工作流程。

·  userlist.xml儲存每個使用者的id。

·  readUserList到/data/system/users下解析id.xml,將最終得到的資訊儲存在UserInfo物件中。

原來使用者資訊由UserInfo表達,下面是UserInfo的定義。

[-->UserInfo]

public class UserInfo implements Parcelable {

   //主使用者,全系統只能有一個這樣的使用者

    publicstatic final int FLAG_PRIMARY = 0x00000001;

 

    //管理員,可以建立、刪除其他使用者資訊

    publicstatic final int FLAG_ADMIN   =0x00000002;

    //訪客使用者

    publicstatic final int FLAG_GUEST   =0x00000004;

    publicint id; //id

    publicString name;//使用者名稱

    publicint flags; //屬性標誌

    ......//其他函式

}

UserInfo資訊比較簡單,筆者覺得UserManager的功能暫時還不能企業使用者的需求。感興趣的讀者不妨關注Android未來版本在此方面的變化。

2.  installPackageForAllUsers分析

PKMS在掃描非系統APK的時候,每掃描完一個APK都會呼叫installPackageForAllUsers,呼叫程式碼如下:

mUserManager.installPackageForAllUsers(pkgName,pkg.applicationInfo.uid);

[-->UserManager.java::installPackageForAllUsers]

public void installPackageForAllUsers(StringpackageName, int uid) {

 for (intuserId : mUserIds) {

   if(userId == 0)

      continue;

  //向installd傳送命令,其中getUid將組合userId和uid為一個整型值

  //installd將在/data/對應user/目錄下建立相應的package子目錄

  mInstaller.createUserData(packageName, PackageManager.getUid(userId,uid),

                                  userId);

        }

 }

 

4.7  本章學習指導

PKMS是本書分析的第一個重要核心服務,其中的程式碼量,關聯的知識點,涉及的資料結構都比較多。這裡提出一些學習建議供讀者參考。

·  從工作流程上看,PKMS包含幾條重要的主線。一條是PKMS自身啟動時建構函式的工作流程,另外幾條和APK安裝、解除安裝相關。每一條主線的難度都比較大,讀者可結合日常工作的需求進行單獨研究,例如研究如何加快建構函式的執行時間等。

·  從資料結構上看,PKMS涉及非常多的資料型別。如果對每個資料結構進行孤立分析,很容易陷入不可自拔的狀態。筆者建議不妨跳出各種資料結構的具體形態,只從目的及功能角度去考慮。這裡需要讀者仔細檢視前面的重要資料結構及說明示意圖。

另外,由於篇幅所限,本章還有一些內容並沒有涉及,需要讀者在學習本章內容的基礎上自行研究。這些內容包括:

·  APK安裝在SD卡,以及APK從內部儲存轉移到SD卡的流程。

·  和Package相關的內容,例如簽名管理、dex優化等。

4.8  本章小結

本章對PackageManagerService進行了較深入的分析。首先分析了PKMS建立時建構函式的工作流程;接著以APK安裝為例,較詳細地講解了這個複雜的處理流程;然後又介紹了PKMS另外一項功能,即根據Intent查詢匹配的Activities;最後,介紹了與installd和UserManager有關的知識。



[①] Signature和Android安全機制有關。本系列書後續擬考慮編寫有關Android安全方面的專題卷。

相關文章