原文链接:https://blog.csdn.net/Innost/article/details/47253179
本章主要内容:
详细分析PackageManagerService

1  概述

PackageManagerService是本书分析的第一个核心服务,也是Android系统中最常用的服务之一。它负责系统中Package的管理,应用程序的安装、卸载、信息查询等。图1展示了PackageManagerService及客户端的类家族。

图1  PackageManagerService及客户端类家族
由图1可知:
·  IPackageManager接口类中定义了服务端和客户端通信的业务函数(通过Proxy),还定义了内部类Stub,Stub类从Binder派生并实现了IPackageManager接口。
·  PackageManagerService继承自IPackageManager.Stub类,由于Stub类从Binder派生,因此PackageManagerService将作为服务端参与Binder通信。
·  Stub类中定义了一个内部类Proxy,该类有一个IBinder类型(实际类型为BinderProxy)的成员变量mRemote,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.java文件。
[-->IPackageManager.java]

public interface IPackageManager extends android.os.IInterface {//定义内部类Stub,派生自Binder,实现IPackageManager接口public static abstract class Stub extends android.os.Binder implements android.content.pm.IPackageManager {private static final java.lang.String DESCRIPTOR = "android.content.pm.IPackageManager";public Stub() {this.attachInterface(this,DESCRIPTOR);}......//定义Stub的内部类Proxy,实现IPackageManager接口private static class Proxy implements android.content.pm.IPackageManager{//通过mRemote变量和服务端交互private android.os.IBinder mRemote;Proxy(android.os.IBinderremote) {mRemote = remote;}......}......}
}

接下来分析PackageManagerService,为书写方便起见,以后将其简称为PKMS。

2  初识PackageManagerService

PKMS作为系统的核心服务,由SystemServer创建,相关代码如下:
[-->SystemServer.java]

//ServerThread的run函数
/*
4.0新增的一个功能,即设备加密(encrypting the device),该功能由系统属性vold.decrypt指定。这部分功能比较复杂,本书暂不讨论。
该功能对PKMS的影响就是通过onlyCore实现的,该变量用于判断是否只扫描系统库(包括APK和Jar包)
*/
String cryptState = SystemProperties.get("vold.decrypt");
boolean onlyCore = false;
//ENCRYPTING_STATE的值为"trigger_restart_min_framework"
if(ENCRYPTING_STATE.equals(cryptState)) {......onlyCore = true;
} else if(ENCRYPTED_STATE.equals(cryptState)) {......onlyCore = true;
}
//1 调用PKMS的main函数,第二个参数用于判断是否为工厂测试,我们不讨论的这种情况,假定onlyCore的值为false
pm = PackageManagerService.main(context, factoryTest !=SystemServer.FACTORY_TEST_OFF,onlyCore);
boolean firstBoot = false;
try {//判断本次是否为初次启动。这里的FirstBoot是指开机后的第一次启动firstBoot = pm.isFirstBoot();
}
......
try {//2 做dex优化,dex是Android上针对Java字节码的一种优化技术,可提高运行效率pm.performBootDexOpt();
}
......
try {//3 通知系统进入就绪状态pm.systemReady();
}
......
}//run函数结束

以上代码中共有4个关键调用,分别是:
·  PKMS的main函数。这个函数是PKMS的核心,稍后会重点分析它。
·  isFirstBoot、performBootDexOpt和systemReady。这3个函数比较简单。学完本章后,读者可完全自行分析它们,故这里不再赘述。
首先分析PKMS的main函数,它是核心函数,此处单独用一节进行分析。

3  PKMS的main函数分析

PKMS的main函数代码如下:
[-->PackageManagerService.java]

public static final IPackageManager main(Contextcontext, boolean factoryTest, boolean onlyCore) {//调用PKMS的构造函数,factoryTest和onlyCore的值均为falsePackageManagerService m = new PackageManagerService(context, factoryTest, onlyCore);//向ServiceManager注册PKMSServiceManager.addService("package", m);return m;
}

main函数很简单,只有短短几行代码,执行时间却较长,主要原因是PKMS在其构造函数中做了很多“重体力活”,这也是Android启动速度慢的主要原因之一。在分析该函数前,先简单介绍一下PKMS构造函数的功能。
PKMS构造函数的主要功能是,扫描Android系统中几个目标文件夹中的APK,从而建立合适的数据结构以管理诸如Package信息、四大组件信息、权限信息等各种信息。抽象地看,PKMS像一个加工厂,它解析实际的物理文件(APK文件)以生成符合自己要求的产品。例如,PKMS将解析APK包中的AndroidManifest.xml,并根据其中声明的Activity标签来创建与此对应的对象并加以保管。PKMS的工作流程相对简单,复杂的是其中用于保存各种信息的数据结构和它们之间的关系,以及影响最终结果的策略控制(例如前面代码中的onlyCore变量,用于判断是否只扫描系统目录)。
PKMS构造函数的工作流程大体可分三个阶段:
·  扫描目标文件夹之前的准备工作。
·  扫描目标文件夹。
·  扫描之后的工作。
该函数涉及到的知识点较多,代码段也较长,因此我们将通过分段讨论的方法,集中解决相关的重点问题。

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();//1 addSharedUserLPw是什么?马上来分析mSettings.addSharedUserLPw("android.uid.system",Process.SYSTEM_UID, ApplicationInfo.FLAG_SYSTEM);mSettings.addSharedUserLPw("android.uid.phone",MULTIPLE_APPLICATION_UIDS ? 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,值为1000ApplicationInfo.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有不同权限)。一个进程也可分属不同的用户组(每个用户组都有对应的权限)。
在Android平台中,系统定义的UID/GID在Process.java文件中,如下所示:
[-->Process.java]

   //系统进程使用的UID/GID,值为1000public static final int SYSTEM_UID = 1000;//Phone进程使用的UID/GID,值为1001public static final int PHONE_UID = 1001;//shell进程使用的UID/GID,值为2000public static final int SHELL_UID = 2000;//使用LOG的进程所在的组的UID/GID为1007public static final int LOG_UID = 1007;//供WIF相关进程使用的UID/GID为1010public static final int WIFI_UID = 1010;//mediaserver进程使用的UID/GID为1013public static final int MEDIA_UID = 1013;//设置能读写SD卡的进程的GID为1015public static final int SDCARD_RW_GID = 1015;//NFC相关的进程的UID/GID为1025public static final int NFC_UID = 1025;//有权限读写内部存储的进程的GID为1023public static final int MEDIA_RW_GID = 1023;//第一个应用Package的起始UID为10000public static final int FIRST_APPLICATION_UID = 10000;//系统所支持的最大的应用Package的UID为99999public static final int LAST_APPLICATION_UID = 99999;//和蓝牙相关的进程的GID为2000public static final int BLUETOOTH_GID = 2000;

下面分析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为uids = 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对象。

(2) SharedUserSetting分析    
该例子来源于SystemUI的AndroidManifest.xml,如下所示:
[-->SystemUI的AndroidManifest.xml]

<manifest xmlns: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。
来看Android是如何设计相应数据结构的,如图2所示:

图2  SharedUserSetting类的关系图
由图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所在进程小于10000if(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;
}

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对象WindowManager wm = (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);//1 从文件中读权限readPermissions();//2 readLPw分析mRestoredSettings = mSettings.readLPw();long startTime = SystemClock.uptimeMillis();

以上代码中调用了两个函数,分别是readPermission和Setttings的readLPw,它们有什么作用呢?
(1) readPermissions函数分析
先来分析readPermissions函数,从其函数名可猜测到它和权限有关,代码如下:
[-->PackageManagerService.java]

void readPermissions() {// 指向/system/etc/permission目录,该目录中存储了和设备相关的一些权限信息File libraryDir = 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文件呢?如图3所示。

图3  /system/etc/permissions目录下的内容
[-->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-permission name="android.permission.READ_CALENDAR" uid="shell" />......<!-- 系统提供的Java库,应用程序运行时候必须要链接这些库,该工作由系统自动完成 --><library name="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用于指定系统库。当应用程序运行时,系统会自动为这些进程加载这些库。
真实设备上/system/etc/permission目录中的文件是从哪里的呢?
答案是,在编译阶段由不同硬件平台根据自己的配置信息复制相关文件到目标目录中得来的。这里给出一个例子,如图4所示:

图4  /system/etc/permission目录中文件的来源
了解了与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);}......} ......} ......
}

总结相关的数据结构,如图4所示。在每个类图中,首行是数据结构名,第二行是数据结构的类型,第三行是注释。图4中各种数据结构的目的是为了保存XML中各种标签及它们之间的关系。

图4  通过readPermissions函数建立的数据结构及其关系
(2) readLPw的“佐料”
readLPw函数的功能也是解析文件,不过这些文件的内容却是在PKMS正常启动后生成的。这里仅介绍作为readLPw“佐料”的文件的信息。文件的具体位置在Settings构造函数中指明,其代码如下:
[-->Settings.java]

Settings() {File dataDir = Environment.getDataDirectory();File systemDir = 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文件的内容,然后建立并更新对应的数据结构。

3.  第一阶段工作总结

扫描并解析XML文件,将其中的信息保存到特定的数据结构中。

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");
boolean didDexOpt = 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;}} ......}} ......//将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优化。处理方式同上}
}

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下的packagescanDirLI(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下的packagescanDirLI(mVendorAppDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR, scanMode, 0);//和installd交互。以后单独分析installdmInstaller.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目录。
(1) scanDirLI函数分析
scanDirLI函数的代码如下:
[-->PackageManagerService.java]

private void scanDirLI(File dir, int flags, int scanMode, long currentTime) {String[] files = dir.list();//列举该目录下的文件......int i;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函数。
(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信息。对比这两次的信息就知道是否需要做升级了。这部分代码比较繁琐,但不影响我们正常分析。感兴趣的读者可自行研究*///判断是否需要设置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]

public Package parsePackage(File sourceFile, String destCodePath,DisplayMetrics metrics, int flags) {mParseError = PackageManager.INSTALL_SUCCEEDED;mArchiveSourcePath = sourceFile.getPath();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);......int type;......//以pkgName名字为参数,创建一个Package对象。后面的工作就是解析XML并填充该Package信息finalPackage pkg = new Package(pkgName);boolean foundApp = false;......//下面开始解析该文件中的标签,由于这段代码功能简单,所以这里仅列举相关函数while(如果解析未完成){......String tagName = 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());}} else if (tagName.equals("uses-configuration")){/*该标签用于指明本package对硬件的一些设置参数,目前主要针对输入设备(触摸屏、键盘等)。游戏类的应用可能对此有特殊要求。*/ConfigurationInfocPref = new ConfigurationInfo();......//解析该标签所支持的各种属性pkg.configPreferences.add(cPref);//保存到Package的configPreferences数组}......//对其他标签解析和处理}
}

上面代码展示了AndroidManifest.xml解析的流程,其中比较重要的函数是parserApplication,它用于解析application标签及其子标签(Android的四大组件在application标签中声明)。
图5表示了PackageParser及其内部重要成员的信息。

图5  PackageParser大家族
由图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。
(4) 与scanPackageLI再相遇
在PackageParser扫描完一个APK后,此时系统已经根据该APK中AndroidManifest.xml,创建了一个完整的Package对象,下一步就是将该Package加入到系统中。此时调用的函数就是另外一个scanPackageLI,其代码如下:
[-->PackageManagerService.java::scanPackageLI函数]

private PackageParser.PackagescanPackageLI(PackageParser.Package pkg,int parseFlags, int scanMode, long currentTime) {File scanFile = new File(pkg.mScanPath);......mScanningPath = scanFile;//设置package对象中applicationInfo的flags标签,用于标示该Package为系统Packageif((parseFlags&PackageParser.PARSE_IS_SYSTEM) != 0) {pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;}//1 下面这句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成员指向mResolveActivitymResolveInfo.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,有图为证,如图6所示为该APK的AndroidManifest.xml中的相关内容。

图6  framework-res.apk的AndroidManifest.xml
该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。
继续对scanPackageLI函数的分析。
[-->PackageManagerService::scanPackageLI函数]

   ......//mPackages用于保存系统内的所有Package,以packageName为keyif(mPackages.containsKey(pkg.packageName) || mSharedLibraries.containsKey(pkg.packageName)) {return null;}File destCodeFile = new File(pkg.applicationInfo.sourceDir);File destResourceFile = new File(pkg.applicationInfo.publicSourceDir);SharedUserSettingsuid = null;//代表该Package的SharedUserSetting对象PackageSetting pkgSetting = null;//代表该Package的PackageSetting对象synchronized(mPackages) {......//此段代码大约有300行左右,主要做了以下几方面工作/*1 如果该Packge声明了” uses-librarie”话,那么系统要判断该library是否在mSharedLibraries中2 如果package声明了SharedUser,则需要处理SharedUserSettings相关内容,由Settings的getSharedUserLPw函数处理3 处理pkgSetting,通过调用Settings的getPackageLPw函数完成4 调用verifySignaturesLP函数,检查该Package的signature*/}final long scanFileTime = scanFile.lastModified();final boolean forceDex = (scanMode&SCAN_FORCE_DEX) != 0;//确定运行该package的进程的进程名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文件进行扫描,如图7所示为该函数的调用流程。

图7  scanDirLI工作流程总结
扫描完APK文件后,Package的私有财产就充公了。PKMS提供了好几个重要数据结构来保存这些财产,这些数据结构的相关信息如图8所示。

图8  PKMS中重要的数据结构
图8用UML的类图来表示PKMS中重要的数据结构。每个类图的第一行为成员变量名,第二行为数据类型,第三行为注释说明。

3.  扫描非系统Package

非系统Package就是指那些不存储在系统目录下的APK文件,这部分代码如下:
[-->PackageManagerService.java::构造函数第三部分]

           if (!mOnlyCore) {//mOnlyCore用于控制是否扫描非系统PackageIterator<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可以启动多个线程以扫描不同的目录,但是目前代码中还没有寻找到相关的蛛丝马迹。难道此处真的就不能优化了吗?读者如果有更好的解决方案,不妨和大家分享一下。

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构造函数返回
}

3.4  PKMS构造函数总结

从流程角度看,PKMS构造函数的功能还算清晰,无非是扫描XML或APK文件,但是其中涉及的数据结构及它们之间的关系却较为复杂。这里有一些建议供读者参考:
·  理解PKMS构造函数工作的三个阶段及其各阶段的工作职责。
·  了解PKMS第二阶段工作中解析APK文件的几个关键步骤,可参考图7。
·  了解重点数据结构的名字和大体功能。
如果对PKMS的分析到此为止,则未免有些太小视它了。下面将分析几个重量级的知识点,期望能带领读者全方位认识PKMS。

4  APK Installation分析

本节将分析APK的安装及相关处理流程,从adb install开始。

4.1  adb install分析

adb install有多个参数,这里仅考虑最简单的,如adb install frameworktest.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。static const char *const DATA_DEST = "/data/local/tmp/%s";static const char *const SD_DEST = "/sdcard/tmp/%s";const char* where = DATA_DEST;char apk_dest[PATH_MAX];char verification_dest[PATH_MAX];char* apk_file;char* verification_file = NULL;int file_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 */);...... //1 4.0新增了一个安装包Verification功能,相关知识稍后分析//2 执行pm命令,这个函数很有意思pm_command(transport,serial, argc, argv);......cleanup_apk://3 在手机中执行shell rm 命令,删除刚才传送过去的目标APK文件。为什么要删除呢delete_file(transport, serial, apk_dest);return err;
}

以上代码中共有三个关键点,分别是:
·  4.0新增了APK安装过程中的Verification的功能。其实就是在安装时,把相关信息发送给指定的Verification程序(另外一个APK),由它对要安装的APK进行检查(Verify)。这部分内容在后面分析APK 安装时会介绍。
·  调用pm_command进行安装,这是一个比较有意思的函数,稍后对其进行分析。
·  安装完后,执行shell rm删除刚才传送给手机的APK文件。为什么会删除呢?因为PKMS在安装过程中会将该APK复制一份到/data/app目录下,所以/data/local/tmp下的对应文件就可以删除了。这部分代码在后面也能见到。
先来分析pm_command命令。为什么说它很有意思呢?

4.2  pm分析

pm_command代码如下:
[-->commandline.c]

static int pm_command(transport_type transport,char* serial,int argc, char** argv)
{char buf[4096];snprintf(buf,sizeof(buf), "shell:pm");......//准备参数//发送"shell:pm install 参数"给手机端的adbdsend_shellcommand(transport, serial, buf);return 0;
}

手机端的adbd在收到客户端发来的shellpm命令时会启动一个shell,然后在其中执行pm。pm是什么?为什么可以在shell下执行?
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函数。在分析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) {new Pm().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() {int installFlags = 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{//1 调用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.3  installPackageWithVerification函数分析

installPackageWithVerification的代码如下:
[-->PackageManagerService.java::installPackageWithVerification函数]

public void installPackageWithVerification(UripackageURI,IPackageInstallObserverobserver,int flags, String installerPackageName, Uri verificationURI,ManifestDigest manifestDigest) {//检查客户端进程是否具有安装Package的权限。在本例中,该客户端进程是shellmContext.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,其基类是HandlerParamsmsg.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函数} ......
}void doHandleMessage(Message msg) {switch(msg.what) {case INIT_COPY: {//1 这里记录的是params的基类类型HandlerParams,实际类型为InstallParamsHandlerParams params = (HandlerParams) msg.obj;//idx为当前等待处理的安装请求的个数int idx = 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) {HandlerParams params = 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及其成员,如图8所示。

图8  HandlerParams及InstallArgs家族成员
由图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。
在前面MCS_BOUND的处理中,首先调用InstallParams的startCopy函数,该函数由其基类HandlerParams实现,代码如下:
[-->PackageManagerService.java::HandlerParams.startCopy]

final boolean startCopy() {boolean res;try {//MAX_RETIRES目前为4,表示尝试4次安装,如果还不成功,则认为安装失败if(++mRetries > MAX_RETRIES) {mHandler.sendEmptyMessage(MCS_GIVE_UP);handleServiceError();return false;} else {handleStartCopy();//1 调用派生类的handleStartCopy函数res= true;}} ......handleReturnCode();//2 调用派生类的handleReturnCode,返回处理结果return res;
}

在上述代码中,基类的startCopy将调用子类实现的handleStartCopy和handleReturnCode函数。下面来看InstallParams是如何实现这两个函数的。

(2) InstallParams分析

先来看派生类InstallParams的handleStartCopy函数,代码如下:
[-->PackageManagerService::InstallParams.handleStartCopy]

public void handleStartCopy() throwsRemoteException {int ret= PackageManager.INSTALL_SUCCEEDED;//根据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;} 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);//1 调用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;}//2 根据DCS返回的安装路径,还需要调用installLocationPolicy进行检查loc = installLocationPolicy(pkgLite, flags);if(!onSd && !onInt) {if(loc == PackageHelper.RECOMMEND_INSTALL_EXTERNAL) {flags |= PackageManager.INSTALL_EXTERNAL;flags &=~PackageManager.INSTALL_INTERNAL;} ......//处理安装位置为内部存储的情况}}//3 创建一个安装参数对象,对于安装位置为内部存储的情况,args的真实类型为FileInstallArgsfinalInstallArgs args = createInstallArgs(this);mArgs = args;if (ret == PackageManager.INSTALL_SUCCEEDED) {final int requiredUid = mRequiredVerifierPackage == null ? -1:getPackageUid(mRequiredVerifierPackage);if(requiredUid != -1 && isVerificationEnabled()) {......//4 待会再讨论verification的处理} else {//5 调用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, long threshold) {//注意该函数的参数: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) {//解析失败......//设置错误值return ret;}ret.packageName = pkg.packageName;ret.installLocation = pkg.installLocation;ret.verifiers = pkg.verifiers;//调用recommendAppInstallLocation,取得一个合理的安装位置ret.recommendedInstallLocation = recommendAppInstallLocation(pkg.installLocation, archiveFilePath, flags, threshold);return ret;
}

APK可在AndroidManifest.xml中声明一个安装位置,不过DCS除了解析该位置外,还需要做进一步检查,这个工作由recommendAppInstallLocation函数完成。
(2) InstallArgs的copyApk函数分析
至此,我们已经得到了一个合适的安装位置(先略过Verification这一步)。下一步工作就由copyApk来完成。根据函数名可知该函数将完成APK文件的复制工作,此中会有蹊跷吗?来看下面的代码。
[-->PackageManagerService.java::InstallArgs.copyApk]

int copyApk(IMediaContainerService imcs, boolean temp) throws RemoteException {if (temp){/*本例中temp参数为true,createCopyFile将在/data/app下创建一个临时文件。临时文件名为vmdl-随机数.tmp。为什么会用这样的文件名呢?因为PKMS通过Linux的inotify机制监控了/data/app,目录,如果新复制生成的文件名后缀为apk,将触发PKMS扫描。为了防止发生这种情况,这里复制生成的文件才有了如此奇怪的名字*/createCopyFile();}File codeFile = 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-随机数.tmpret= imcs.copyResource(packageURI, out);} finally {......//关闭out,撤销URI授权}return ret;
}

关于临时文件,这里提供一个示例,如图9所示。

图9  createCopyFile生成的临时文件
由图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);}
}private void processPendingInstall(finalInstallArgs args, final intcurrentStatus) {//向mHandler中抛一个Runnable对象mHandler.post(new Runnable() {public void 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) {//1 调用FileInstallArgs的doPreInstallargs.doPreInstall(res.returnCode);synchronized (mInstallLock) {//2 调用installPackageLI进行安装installPackageLI(args, true, res);}//3 调用FileInstallArgs的doPostInstallargs.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为keymRunningInstalls.put(token, data);if (res.returnCode ==PackageManager.INSTALL_SUCCEEDED && doRestore) {......//备份恢复的情况暂时不考虑}if(!doRestore) {//4 抛一个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文件。至此,该APK中的私有财产就全部被登记到PKMS内部进行保存了。
·  调用InstallArgs的doPostInstall函数,在本例中是FileInstallArgs的doPostInstall函数。
·  此时,该APK已经安装完成(不论失败还是成功),继续向mHandler抛送一个POST_INSTALL消息,该消息携带一个token,通过它可从mRunningInstalls数组中取得一个PostInstallData对象。

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);boolean deleteOld = 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  APK 安装流程总结

没想到APK的安装流程竟然如此复杂,其目的无非是让APK中的私人财产公有化。
这里要总结APK安装过程中的几个重要步骤,如图10所示。

图10  APK安装流程
图10中列出以下内容:
·  安装APK到内部存储空间这一工作流程涉及的主要对象包括:PKMS、DCS、InstallParams和FileInstallArgs。
·  此工作流程中每个对象涉及到的关键函数。
·  对象之间的调用通过虚线表达,调用顺序通过①②③等标明。
todo 这个UML顺序图不标准

5  queryIntentActivities分析

PKMS除了负责Android系统中Package的安装、升级、卸载外,还有一项很重要的职责,就是对外提供统一的信息查询功能,其中包括查询系统中匹配某Intent的Activities、BroadCastReceivers或Services等。本节将以查询匹配某Intent的Activities为例,介绍PKMS在这方便提供的服务。
正式分析queryIntentActivities之前,先来认识一下Intent及IntentFilter。

5.1  Intent及IntentFilter介绍

1.  Intent介绍

Intent中文是“意图”的意思,它是Android系统中一个很重要的概念,其基本思想来源于对日常生活及行为的高度抽象。
意图,是一个非常抽象的概念,在编码设计中,如何将它实例化呢?Android系统明确指定的一个Intent可由两方面属性来衡量。
·  主要属性:包括Action和Data。其中Action用于表示该Intent所表达的动作意图、Data用于表示该Action所操作的数据。
·  次要属性:包括Category、Type、Component和Extras。其中Category表示类别,Type表示数据的MIME类型,Component可用于指定特定的Intent响应者(例如指定广播接收者为某Package的某个BroadcastReceiver),Extras用于承载其他的信息。
进一步对Intent进行分类:
·  Explicit Intents:这类Intent明确指明了要找哪些人。在代码中通过setComponent或setClass来锁定目标对象。处理这种Intent,工作就很轻松了。
·  Implicit Intents:这一类Intents只标明了工作内容,而没有指定具体人名。

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中的说明。

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");//1 加到mActivities中保存
}

上面的代码中有两个比较重要的数据结构,如图11所示:

图11  相关数据结构示意图
·  ActivityIntentResolver数据结构内部有一个mActivities变量,它以ComponetName为Key,保存PackageParser.Activity对象
·  从APK中解析得到的所有和Activity相关的信息(包括在XML中声明的IntentFilter标签)都由PacakgeParser.Activity来保存。
前面代码中调用addActivity函数完成了私有信息的公有化。addActivity函数的代码如下:
[-->PacakgeManagerService.java::ActivityIntentResolver.addActivity]

public final void addActivity(PackageParser.Activity a, String type) {final boolean systemApp = isSystemApp(a.info.applicationInfo);//将Component和Activity保存到mActivities中mActivities.put(a.getComponentName(), a);final int 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函数的最后一个参数用于打印信息int numS = register_intent_filter(f, f.schemesIterator(), mSchemeToFilter, "      Scheme: ");int numT = 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"><action android:name="android.intent.action.VIEW" />data android:mimeType="audio/*" android:scheme="http"
</intent-filter>

那么:
·  在mTypedActionToFilter中能够以“android.intent.action.VIEW”为key找到该IntentFilter。
·  在mWildTypeToFilter和mTypeToFilter中能够以“audio”为key找到该IntentFilter。
·  在mSchemeToFilter中能够以”http“为key找到该IntentFilter。
下面来分析Intent匹配查询工作。

5.3  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函数的代码。
下面来看PKMS对匹配查询的处理。

2.  queryIntentActivities分析

该函数代码如下:
[-->PacakgeManagerService.java::queryIntentActivities]

public List<ResolveInfo>queryIntentActivities(Intent intent,String resolvedType, int flags) {final ComponentName comp = intent.getComponent();if(comp != null) {//1 Explicit的Intents,直接根据component得到对应的ActivityInfofinal List<ResolveInfo> list = new ArrayList<ResolveInfo>(1);final ActivityInfo ai = getActivityInfo(comp, flags);if (ai != null) {final ResolveInfo ri = new ResolveInfo();//ResovlerInfo的activityInfo指向查询得到的ActivityInfori.activityInfo = ai;list.add(ri);}return list;}synchronized (mPackages) {final String pkgName = intent.getPackage();if (pkgName == null) {//3 Implicit Intents,我们重点分析此中情况return mActivities.queryIntent(intent, resolvedType, flags);}//Intent指明了PackageName,比Explicit Intents情况差一点final PackageParser.Package pkg = mPackages.get(pkgName);if (pkg != null) {//2 其实是从该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函数return super.queryIntent(intent, resolvedType, (flags&PackageManager.MATCH_DEFAULT_ONLY) != 0);
}

基类queryIntent里设置了最多四轮匹配关卡,然后逐一执行匹配工作。具体的匹配代码由buildResolveList完成。

6  installd介绍

在前面对PKMS构造函数分析时介绍过一个Installer类型的对象mInstaller,它通过socket和后台服务installd交互,以完成一些重要操作。这里先回顾一下PKMS中mInstaller的调用方法:

//创建一个Installer对象
mInstaller = new Installer();
//对某个APK文件进行dexopt优化
mInstaller.dexopt(paths[i], Process.SYSTEM_UID,true);
//扫描完系统Package后,调用moveFiles函数
mInstaller.moveFiles();
//当存储空间不足时,调用该函数清理存储空间
mInstaller.freeCache(freeStorageSize);

Installer的种种行为都和其背后的installd有关。下面来分析installd。

6.1 installd概貌

installd是一个native进程,代码非常简单,其功能就是启动一个socket,然后处理来自Installer的命令,其代码如下:
[-->installd.c]

int main(const int argc, const char *argv[]) {char buf[BUFFER_MAX];struct sockaddr addr;socklen_t alen;int lsocket, 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);}return 0;
}

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 },
};

下面来分析相关的几个命令。

6.2  dexOpt命令分析

PKMS在需要对一个APK或jar包做dex优化时,会发送dexopt命令给installd,相应的处理函数为do_dexopt,代码如下:
[-->installd.c]

static int do_dexopt(char **arg, charreply[REPLY_MAX])
{return dexopt(arg[0], atoi(arg[1]), atoi(arg[2]));
}

[-->commands.c]

int dexopt(const char *apk_path, uid_t uid, intis_public)
{struct utimbuf ut;struct stat apk_stat, dex_stat;char dex_path[PKG_PATH_MAX];char dexopt_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_t pid;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/目录中。这里给出一个示例,如图12所示。

图12  dex文件示例
提示 dexopt进程由android源码/dalvik/dexopt/OptMain.cpp定义。感兴趣的读者可深入研究dex优化的工作原理。

6.3  movefiles命令分析

PKMS扫描完系统Package后,将发送该命令给installd,相应处理函数的代码如下:
[-->installd.c]

static int do_movefiles(char **arg, char reply[REPLY_MAX])
{return movefiles();
}

[-->commands.c]

int movefiles()
{DIR *d;int dfd,subfd;struct dirent *de;struct stat s;char buf[PKG_PATH_MAX+1];int bufp, bufe, bufi, readlen;char srcpkg[PKG_NAME_MAX];char dstpkg[PKG_NAME_MAX];char srcpath[PKG_PATH_MAX];char dstpath[PKG_PATH_MAX];int dstuid=-1, dstgid=-1;int hasspace;//打开/system/etc/updatecmds/目录d = opendir(UPDATE_COMMANDS_DIR_PREFIX);if (d ==NULL) {goto done;}dfd = dirfd(d);while((de = readdir(d))) {......//解析该目录下的文件,然后执行对应操作}closedir(d);
done:return 0;
}

movefiles的功能和系统升级有关。

6.4  doFreeCache

当系统空间不够时,DSMS会调用PKMS的freeStorageAndNotify函数进行空间清理。该工作真正的实施者是installd,相应的处理命令为do_free_cache,其代码如下:
[-->installd.c]

static int do_free_cache(char **arg, char reply[REPLY_MAX])
{return free_cache((int64_t)atoll(arg[0]));
}

[-->commands.c]

int free_cache(int64_t free_size)
{const char *name;int dfd,subfd;DIR *d;struct dirent *de;int64_t avail;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;//清理空间后,仍然不满足要求
}

7  本章学习指导

这里提出一些学习建议供读者参考。
·  从工作流程上看,PKMS包含几条重要的主线。一条是PKMS自身启动时构造函数的工作流程,另外几条和APK安装、卸载相关。每一条主线的难度都比较大,读者可结合日常工作的需求进行单独研究,例如研究如何加快构造函数的执行时间等。
·  从数据结构上看,PKMS涉及非常多的数据类型。如果对每个数据结构进行孤立分析,很容易陷入不可自拔的状态。笔者建议不妨跳出各种数据结构的具体形态,只从目的及功能角度去考虑。这里需要读者仔细查看前面的重要数据结构及说明示意图。

[2021.8.18]深入理解PackageManagerService相关推荐

  1. 实战:怎样把间隔的几个commit整理成1个?-2021.11.18

    实战:怎样把间隔的几个commit整理成1个?-2021.11.18 目录 文章目录 实战:怎样把间隔的几个commit整理成1个?-2021.11.18 目录 文档来源 实验环境 实验软件(无) 1 ...

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

    由于<深入理解Android 卷一>和<深入理解Android卷二>不再出版,而知识的传播不应该因为纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容 第 ...

  3. 2.4 深入理解PackageManagerService

    第4章 深入理解PackageManagerService 4.1 概述 PackageManagerService是本书分析的第一个核心服务,也是Android系统中最常用的服务之一.它负责系统中P ...

  4. 2021年 第12届 蓝桥杯 Java B组 省赛真题详解及小结【第1场省赛 2021.04.18】

    蓝桥杯 Java B组 省赛决赛 真题详解及小结汇总[题目下载.2013年(第4届)~2020年(第11届)] CSDN 蓝桥杯 专栏 2013年 第04届 蓝桥杯 Java B组 省赛真题详解及小结 ...

  5. ElasticSearch搜索引擎-2_学习笔记2021.4.18)

    ElasticSearch搜索引擎-2_学习笔记(2021.4.18) 前言: RESTful 介绍 , 我们通过RESTful来操作ElasticSearch (所有请求都是通过Postman ) ...

  6. 【Doris Weekly FAQ】2021.07.05~2021.07.18

    观众朋友们: 晚上好! 欢迎收看[ Doris 近日要闻]~本次为您带来的是 2021年07月15日 - 2021年07月18日 的双周总结. Doris 社区周报每期会包含 FAQ 环节.我们会在社 ...

  7. 2021.11.18 简单计算阶乘(多种方法)及 求阶乘的和

    运用各种方法计算阶乘及阶乘之和 三种方法计算阶乘 一.利用for循环法 二.利用导入库法计算阶乘 三.利用函数递归法 在原有基础上计算阶乘之和 学习方向: 三种方法计算阶乘 一.利用for循环法 &q ...

  8. Go 1.18特性快速了解 | Gopher Daily (2021.12.18) ʕ◔ϖ◔ʔ

    每日一谚:感觉Go是谷歌这些年来唯一真正仁慈的产品之一.它没有任何地方看起来对谷歌"行了方便".我没有感觉到别有用心的动机.没有跟踪或锁定.它只是真正的帮助大家,对世界有用且免费. ...

  9. Java知识体系最强总结(2021版),深入理解linux内核架构评价

    CentOS8 推荐书籍 读书笔记 云计算 搜索引擎 权限管理 区块链 Java面试总结 Java面试总结汇总,整理了包括Java基础知识,集合容器,并发编程,JVM,常用开源框架Spring,MyB ...

最新文章

  1. [原创] SQLite数据库使用清单(上)
  2. getpass函数简单使用
  3. 现代软件工程 第十一章 【软件设计与实现】 练习与讨论
  4. Redisson 实现分布式锁
  5. 百度指数查关键词(惊到我啦)
  6. 半空间数据空间化相关接口
  7. 单片机程序框架设计与实现
  8. VSIX 安装失败解决方案
  9. MAC OS读取NTFS格式硬盘
  10. xp无法查看工作组计算机 服务没有启动,XP系统弹出“无法查看工作组计算机”提示怎么办?...
  11. Python爬虫之头条采集免费方法
  12. 常用的SQL语句大全-单表操作
  13. 部分安卓机型无法打开公众号/http链接,部分安卓机型无法打开公众号/http链接白屏
  14. Linux的命令回收站在哪,Trash-Cli:Linux 上的命令行回收站工具
  15. TypeError: torch.FloatTensor is not a Module subclass
  16. 敏捷转型行动笔记:敏捷导入课程培训
  17. 求助威纶通触摸屏与三菱PLC程序
  18. 通达信资金净流入公式_通达信主力资金净流入公式是什么?
  19. 验证tomcat安装成功
  20. 【Alpha】阶段第四次Scrum Meeting

热门文章

  1. 使用GDAL/GEOS求面特征的并集
  2. 基于51单片机的简易数字电压表proteus仿真原理图程序设计
  3. 计算机课堂热身游戏,简单的两款小游戏,带你回味和同学在电脑课上的时光
  4. 8.5.3耳麦拔插事件调用流程分析
  5. 全同态加密(FHE):BV方案、密钥切换、模约化、自举
  6. Lua学习篇③——数据类型
  7. 《我是谁:没有绝对安全的系统》观影感受
  8. Linux系统安全加固设置详细教程
  9. 可以手机安装电脑版chrome插件的安卓浏览器
  10. 代码的坏味道之十三 :Speculative Generality(夸夸其谈未来性)