用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章,技术文章也可以有温度。
我的公众号:牛晓伟
本文摘要
这是包管理 系列的第二篇文章,本篇文章主要介绍记录存储模块 ,通过本文您将了解记录存储模块的作用,apk安装信息是如何记录和存储的。(文中代码基于Android13)
阅读本篇内容之前建议先看深入理解包管理--PackageManagerService和它的"小伙伴"这篇文章。
本文大纲
1. 记录存储模块是个啥
2. apk安装信息的记录与存储
3. 声明权限信息的记录与存储
4. 记录存储模块的初始化
5. 总结
1. 记录存储模块是个啥?
大家好,我是记录存储模块 ,我服务于PackageManagerService服务 ,万事万物都有存在的理由,我也不例外我存在的作用是记录apk的安装及附加信息并且把这些信息存储到文件中。
我也像数据库 一样支持增 、删 、改 、查 操作,当某个apk安装成功后,我会把该apk的包名 、版本号 、apk安装路径 、安装时间 、签名 、请求权限状态信息 等信息记录下来,并且存储到相应文件中;当某个apk被删除后,我会把该apk的相关信息如安装记录信息 、请求权限状态信息等删除掉,并且把存储在文件中的信息也进行删除;当某个apk升级后,我会把该apk相关的信息进行更新,并且把最新信息更新到相应文件中。
我记录并且存储apk安装相关信息的目的是为了让使用者查询 ,比如使用者可以通过包名 从我这查询某个apk是否被安装了,再比如还可以查询某个apk的一些关键信息如apk的版本号等。
我把我所做的这些事情全权交给了Settings类 ,Settings这个类的名字也是我费尽脑细胞才想出来的,我承认这个类的名字起的不好、起的有歧义,大家就先将就将就吧。
2. apk安装信息的记录与存储
那我就把我的主要工作apk安装信息的记录与存储 介绍给大家,apk安装信息的记录与存储 可不是简单的只是把apk的关键信息包名 、版本号 、apk文件路径 等信息记录并存储下来,因为还有其他的信息需要考虑比如用户安装状态 、请求权限状态等。
我把记录apk安装的工作交给了PackageSetting类 。不管是系统apk还是普通apk只要被安装在Android设备上,都会对应一个PackageSetting 实例。比如去酒店入住,工作人员是不是需要把你的身份证、房号等关键信息登记下来。而PackageSetting 的作用正是如此,只要apk被安装在设备上,那就需要初始化一个PackageSetting 实例,该实例会有对应的属性把apk的包名 、apk路径 、安装时间等信息记录下来。
PackageSetting 类负责apk安装信息的记录 ,Settings 类负责apk安装信息的存储 。我特意将PackageSetting 的属性划分为apk基础信息 、不可序列化信息 、apk的用户状态信息 、请求权限状态信息 四部分,不同部分的信息存储的文件是不同的,而且有的信息是不需要存储的。那就从这四部分来进行介绍apk安装信息的记录与存储。
2.1 apk基础信息
apk的基础信息 指的是apk的包名 、appid 、apk文件路径 等信息。如下截取了PackageSetting的部分基础属性:
public class PackageSetting extends SettingBase implements PackageStateInternal {
//apk的包名
private String mName;
//被安装apk对应的appid
private int mAppId;
//被安装apk存放的路径位置
private File mPath;
//apk的版本号
private long versionCode;
//apk的签名信息
private PackageSignatures signatures;
}
上面列出的属性都非常简单,就不做过多的解释了,看下它们的存储。
2.1.1 apk基础信息存储
Settings类会把apk基础信息会被持久化到 /data/system/package.xml 文件中,如下截取了其中两个已安装apk的信息:
# 安装器apk
<package name="com.android.packageinstaller" codePath="/system/priv-app/PackageInstaller"
nativeLibraryPath="/system/priv-app/PackageInstaller/lib" publicFlags="1"
privateFlags="8" ft="18f75d88438" ut="18f75d88438" version="33"
userId="10026" packageSource="0" isOrphaned="true" loadingProgress="1.0"
domainSetId="a4bb03e0-4a89-46ce-b8a9-3ddd3bcb3657">
<sigs count="1" schemeVersion="3">
<cert index="3" />
</sigs>
<proper-signing-keyset identifier="2" />
</package>
# shell apk
<package name="com.android.shell" codePath="/system/priv-app/Shell"
nativeLibraryPath="/system/priv-app/Shell/lib" publicFlags="1" privateFlags="8"
ft="18f75d868e0" ut="18f75d868e0" version="33" sharedUserId="2000" packageSource="0" isOrphaned="true" loadingProgress="1.0"
domainSetId="66ee04a6-2724-4152-b515-c6a83e494a44">
<sigs count="1" schemeVersion="3">
<cert index="3" />
</sigs>
<proper-signing-keyset identifier="2" />
</package>
package.xml 文件存储了已安装apk的基础信息,name 标签存储的是apk的包名,version 标签存储的是apk的版本号,userId 标签存储的是apk的appid (appid在apk安装后就不会发生变化了,如人类的身份证号)。包名、appid这种犹如身份证的信息肯定是需要记录并存储下来的。
而codePath 和nativeLibraryPath 标签分别代表apk文件路径 和native lib的路径 。普通apk 被安装后apk文件是会存放在 /data/app/~~[randomStrA]/[packageName]-[randomStrB](其中randomStrA、randomStrB是随机生成的字符串,packageName是包名)目录下,系统apk 的apk文件都有自己特有的目录如上面packageinstaller 的apk路径是/system/priv-app/PackageInstaller。 而codePath 则记录了该信息,apk文件路径 和native lib的路径 对于app的运行太重要了,在app运行时app的ClassLoader 会把apk文件路径 和native lib的路径放入自己的查找路径中。
sigs标签存储了apk的签名信息,签名信息被存储的主要原因是:首先签名信息是与apk强相关的,只有apk不变签名信息就不会变化,存储下来后就不需要每次都从apk中解析签名信息了 (解析签名信息可是会花不少时间啊);其次签名信息经常会被使用比如当apk升级时,会使用新apk的签名与保存的签名信息进行对比,不一致则说明新apk有问题不可升级。
2.2 不可序列化信息
不可序列化信息 犹如Java中的关键字transient ,transient 的作用是代表它修饰的变量不可被序列化,而apk中也是有一些信息只可被记录而没必要存储 (比如使用共享库信息),而PackageSetting 类中不可序列化信息对应的是PackageStateUnserialized类,也就是说这部分信息是不会存储到文件中的,每次Android设备重新启动的时候都会重新生成。如下是该类的部分属性:
public class PackageSetting extends SettingBase implements PackageStateInternal {
//pkgState对象是PackageStateUnserialized类型
private final PackageStateUnserialized pkgState = new PackageStateUnserialized(this);
}
public class PackageStateUnserialized {
//下面两个是共享库相关的信息,因为共享库是由其他apk提供的,因此这是一个不可控因素,所以没必要存储
private List<SharedLibraryInfo> usesLibraryInfos = emptyList();
private List<String> usesLibraryFiles = emptyList();
省略其他属性······
}
该类中包含的属性主要与共享库 有关,因为共享库是由其他apk提供的,这是一个不可控的因素,其他apk有可能被删除/更新等,所以没必要存储。而是在扫描所有apk的时候在重新收集。
2.3 apk的用户状态信息
首先解释下用户这个概念,此用户是与Android设备相关的,Android设备是可以创建多个用户的,Android设备在默认开启的时候就已经分配了一个默认的用户它的id是0,可以在设置中创建一个新的用户或者访客(假如新用户id是10)。
用户 之间是隔离的,假如设备使用者切换到了id为10的用户 ,这时候使用者安装了A、B、C三个apk,那当使用者切换到了id为0的用户 时,这三个apk并没有被安装。也就是apk是安装在某一用户下的 ,当然这里得严重声明下像系统apk 是针对所有用户都安装的。
而apk的用户状态信息 则记录apk在当前用户下的安装状态 ,安装状态比如是否安装 ,而PackageUserStateImpl 类则记录了apk的用户状态信息,如下是它的相关代码:
public class PackageSetting extends SettingBase implements PackageStateInternal {
//mUserStates可以根据用户id获取相应的PackageUserStateImpl
private final SparseArray<PackageUserStateImpl> mUserStates = new SparseArray<>();
}
public class PackageUserStateImpl extends WatchableImpl implements PackageUserStateInternal,Snappable {
省略属性······
//apk安装后的data目录对应的Inode
private long mCeDataInode;
//是否安装
private boolean mInstalled = true;
//是否停止
private boolean mStopped;
private boolean mNotLaunched;
//是否被隐藏
private boolean mHidden;
//是否是 instant app
private boolean mInstantApp;
}
代码解释
PackageSetting 类的mUserStates 属性记录了所有用户 下apk的安装状态,该属性的key 值是用户 id,而它的value 值是PackageUserStateImpl对象。
PackageUserStateImpl 它的mInstalled 属性记录了当前用户是否安装了该apk,mStopped 属性记录当前用户下apk是停止,而关于mCeDataInode属性会在下面解释。
apk的用户状态信息存储
Settings 类会把apk的用户状态信息 存储在 /data/system/users/userid/package-restrictions.xml 文件中 (路径中的userid指的是用户id,比如0,10),Settings 类会根据当前设备的用户 去对应 userid 目录下加载 package-restrictions.xml 文件。
为了让大家有一个直观体验,我特意把package-restrictions.xml文件的部分内容展示给大家:
/data/system/users/0/package-restrictions.xml (0代表userid为0的用户)
下面三个apk都被安装了
<pkg name="com.baidu.searchbox" ceDataInode="3452" first-install-time="18f76021555"/>
<pkg name="com.android.contacts" ceDataInode="2096" first-install-time="18f75de48c8" />
<pkg name="com.android.camera2" ceDataInode="2099" first-install-time="18f75de3928">
<enabled-components>
<item name="com.android.camera.CameraLauncher" />
</enabled-components>
</pkg>
/data/system/users/10/package-restrictions.xml (10代表userid为0的用户)
# inst为false代表百度apk没有被安装
<pkg name="com.baidu.searchbox" inst="false" first-install-time="0" />
# 下面两个apk都被安装了
<pkg name="com.android.contacts" ceDataInode="48481" first-install-time="0" />
<pkg name="com.android.camera2" ceDataInode="42424" first-install-time="0">
<enabled-components>
<item name="com.android.camera.CameraLauncher" />
</enabled-components>
</pkg>
代码解释
在id为0的用户 上安装了百度、contacts、camera三个apk,而在id为10的用户 上只安装了contacts、camera两个apk。因为contacts、camera两个apk都是系统apk,因此在id为0 和id为10 的用户都安装了,但是它们的ceDataInode值却为啥不同呢?
ceDataInode 代表apk安装后data路径对应的inode值 ,apk被安装在不同用户下的 data路径是 /data/user/userid/packagename (路径中的userid指的是用户id,比如0,10),因此它们的ceDataInode值不同。
2.4 请求权限状态信息
先来介绍下请求权限 ,基本上每个apk或多或少的都会请求一些权限,而请求权限是在AndroidManifest.xml文件中使用uses-permission标签,如下例子:
# 请求外部存储的读权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
请求权限状态 指的是apk请求的权限是否被设备使用者允许 、拒绝 等。apk的所有请求权限状态是需要被记录并且存储下来 的,如果不存储下来,本来已经被允许的请求权限在下次开机的时候该请求权限又会变为默认值,这肯定是不行的。
而请求权限状态信息 也是与用户 有关系的。比如id为0的用户 和id为10的用户 都安装了某个apk,假如切换为id为0的用户 ,这时候使用者为该apk打开了定位权限 。那当使用者切换到id为10的用户 ,这时候该apk的定位权限 肯定还是原先状态。用户之间各种数据都是隔离的。
因此请求权限状态信息 它主要记录了在不同用户下apk的所有请求权限的状态 ,而请求权限的状态有拒绝 、允许 、只给一次 等。LegacyPermissionState类 封装了这些信息,而该类被PackageSetting 的父类SettingBase持有,如下是它的类图
图解
LegacyPermissionState 类拥有一个类型为SparseArray <UserState >的属性mUserStates ,它的key 为userid ,也就是说通过userid 可以拿到UserState对象。
UserState 类拥有一个类型为ArrayMap <String, PermissionState >的属性mPermissionStates ,它的key 是包名,也就是说通过包名能拿到PermissionState对象。
而PermissionState 类存储了一个请求权限 ,它的属性mName代表权限名字。
用一句话总结上面类图,通过PackageSetting 可以拿到LegacyPermissionState 对象,而通过userid 可以拿到每个用户的所有权限对应UserState 对象,而该对象存储了所有的权限,每个权限的key 值是权限名,value值是具体的权限状态信息是PermissionState对象。
请求权限状态信息存储
Settings 类会把请求权限状态信息 存储在 /data/misc_de/userid/apexdata/com.android.permission/runtime-permissions.xml 文件(路径中的userid指的是用户id,比如0,10),Settings 类会根据当前设备的用户 去对应 userid 目录下加载 runtime-permissions.xml 文件。
同样为了让大家有一个直观的体验,我特意把 runtime-permissions.xml 文件 的部分内容展示给大家:
/data/misc_de/0/apexdata/com.android.permission/runtime-permissions.xml (0代表userid为0的用户)
<package name="com.baidu.searchbox">
<package name="com.baidu.searchbox">
<permission name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" granted="true" flags="0" />
# 定位权限 被拒绝
<permission name="android.permission.POST_NOTIFICATIONS" granted="false" flags="301" />
<permission name="android.permission.ACCESS_FINE_LOCATION" granted="false" flags="10301" />
<permission name="android.permission.RESTART_PACKAGES" granted="true" flags="0" />
</package>
/data/misc_de/10/apexdata/com.android.permission/runtime-permissions.xml (10代表userid为0的用户)
<package name="com.baidu.searchbox">
<permission name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" granted="true" flags="0" />
<permission name="android.permission.POST_NOTIFICATIONS" granted="true" flags="301" />
# 定位权限 已允许
<permission name="android.permission.ACCESS_FINE_LOCATION" granted="true" flags="301" />
<permission name="android.permission.RESTART_PACKAGES" granted="true" flags="0" />
</package>
上面的例子比较简单,就不赘述了。
2.5 小结
当一个apk被安装成功后,PackageSetting类 会把apk安装的信息分为apk基础信息 、不可序列化信息 、apk的用户状态信息 、请求权限状态信息四部分来记录。
-
apk基础信息 有包名 、apk版本信息 、appid 、apk文件路径 等,而Settings类会把基础信息存储在 /data/system/package.xml 文件中。
-
不可序列化信息是不需要存储的,而是每次Android设备重新启动的时候需要重新收集。
-
apk的用户状态信息 记录了当前用户 安装apk的状态信息 (状态比如是否安装),而Settings类会把这些信息存储在 /data/system/users/userid/package-restrictions.xml 文件 (路径中的userid指的是用户id,比如0,10)。
-
请求权限状态信息 记录了当前用户 下所有apk的所有申请的权限的状态 (权限的状态比如是否允许),而Settings类会把这些信息会存储在 /data/misc_de/userid/apexdata/com.android.permission/runtime-permissions.xml 文件 (路径中的userid指的是用户id,比如0,10)
在Settings 类中存在类型为WatchedArrayMap <String , PackageSetting >的属性mPackages ,它就代表所有的已安装apk,它的key值为包名,如下代码:
final WatchedArrayMap<String, PackageSetting> mPackages;
当apk安装时,Settings 类会为mPackages 属性增加一个记录,并且把这些信息更新到相应的文件;当apk删除时,Settings 类会从mPackages 把相应的记录删除,并且更新相应的文件;当apk升级时,Settings 类会从mPackages中找到对应记录,更新相应记录信息,并且更新相应文件。
3. 声明权限信息的记录与存储
声明权限信息 需要单独的介绍一下,因为它没有与apk安装信息记录在一起,它可以理解为是apk安装的附加信息。Settings 类会把所有apk声明的所有权限都收集起来,并且存储到文件中 ,这样做的目的是为了服务于权限管理模块 。权限管理模块 它没有存储的功能,每当Android设备重新启动的时候,Settings 类会把所有的声明权限从文件中读取出来,交给权限管理模块 。从这是不是可以看出我记录存储模块 的作用就是记录 与存储 ,权限管理模块 它没有存储的功能,那PackageManagerService服务就把这个事情交给了我。
我先来介绍下声明权限 吧,权限分为声明权限 和使用权限 ,每个apk都可以声明权限,声明的权限是可以被其他apk来使用的。声明权限是在AndroidManifest中使用permission标签,如下代码:
<permission android:description="string resource"
android:icon="drawable resource"
android:label="string resource"
android:name="string"
android:permissionGroup="string"
android:protectionLevel=["normal" | "dangerous" |
"signature" | ...] />
而我把记录所有声明权限 的工作交给了LegacyPermissionSettings类,如下是它的类图
图解
LegacyPermissionSettings 类有一个类型为ArrayMap <String, LegacyPermission >的属性mPermissions ,该属性则代表所有apk声明的所有权限,其中key是权限名。
LegacyPermission 类其中一个属性mPermissionInfo 是PermissionInfo 类型的,而该类的protectionLevel 属性和它的父类的name 、packageName 、labelRes 、icon属性会把声明的权限记录下来。
声明权限信息存储
收集起来的这些权限会被存储在 /data/system/package.xml 文件中,如下截取了部分信息:
<permissions>
<item name="android.permission.LAUNCH_DEVICE_MANAGER_SETUP" package="android" protection="67108866" />
<item name="android.permission.REAL_GET_TASKS" package="android" protection="18" />
<item name="android.permission.ACCESS_CACHE_FILESYSTEM" package="android" protection="18" />
<item name="android.permission.REMOTE_AUDIO_PLAYBACK" package="android" protection="2" />
<item name="android.permission.START_CROSS_PROFILE_ACTIVITIES" package="android" protection="67108866" />
<item name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" package="com.android.providers.downloads" />
<item name="android.permission.NFC_PREFERRED_PAYMENT_INFO" package="android" />
<item name="android.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS" package="android" protection="67108866" />
省略其他部分......
</permissions>
如上会在permissions 标签把所有收集的权限记录下来,每个权限的name是key值,当Android设备启动的时候会从 /data/system/package.xml 文件中把所有声明的权限读取出来。
声明权限信息更新
在Settings 类存在类型为LegacyPermissionSettings 的mPermissions属性,它记录了所有声明的权限,如下代码:
final LegacyPermissionSettings mPermissions;
每当有apk安装后,若apk中存在声明权限,则Settings 类会把这些声明的权限交给mPermissions 属性,并且把最新信息更新到package.xml 文件;当有apk删除后,若apk中存在声明权限,则会从mPermissions 属性中把相应声明权限删除,并且更新到package.xml 文件;当有apk升级后,若新升级apk中删除已声明权限或者增加声明权限,则也会更新mPermissions 属性,并且更新到package.xml文件。
其实还有shared user 信息没有介绍,只是由于android:sharedUserId属性已经过时,故不介绍它了。
4. 记录存储模块的初始化
接下来有必要介绍下我的初始化工作,我是服务于PackageManagerService服务 的,而我所有的事情都交给了Settings类 ,在PackageManagerService 类中会存在类型为Settings 的属性mSettings (如下代码)
final Settings mSettings;
而我的初始化是从PackageManagerService 的构造方法开始的,PackageManagerService 会调用Settings 对象的readLPw方法开始初始化工作,主要做了以下工作:
-
从 /data/system/package.xml 文件中把所有信息都读取出来,并把相应的数据填充给相应的记录类 ,比如遇到package 标签,则会把数据填充给PackageSetting 对象;比如遇到permissions 标签会把每个声明的权限添加到LegacyPermissionSettings对象中。
-
从 /data/system/users/userid/package-restrictions.xml (路径中的userid指的是用户id,比如0,10) 文件中,把每个apk的用户状态信息读取出来
-
从 /data/misc_de/userid/apexdata/com.android.permission/runtime-permissions.xml (userid同上) 文件中,把每个apk的请求权限状态信息读取出来。
一句话总结我的初始化工作就是从文件中把先前存储的信息读取出来,并且填充给相应的类 ,初始化工作完成后,我就可以为PackageManagerService提供服务了。
5. 总结
我是记录存储模块 ,而我服务于PackageManagerService服务 ,我所有的工作都交给了Settings类 ,我的作用是记录apk的安装及附加信息并且把这些信息存储到文件中 ,记录存储的这些信息的主要目的是供使用者来查询 ,比如可以通过包名查询某个apk是否被安装。