??PKMS啟動詳解(三)之BOOT_PROGRESS_PMS_START流程分析
Android PackageManagerService系列博客目錄:
PackageManagerService啟動詳解系列博客概要
PackageManagerService啟動詳解(一)之整體流程分析
PackageManagerService啟動詳解(二)之對已安裝應用怎么進行持久化存盤管理?
PackageManagerService啟動詳解(三)之BOOT_PROGRESS_PMS_START流程分析
引言
??在前面的博客PackageManagerService啟動詳解(二)之對已安裝應用資訊怎么進行持久化存盤管理?中我們特意花了一篇博客的篇幅從設計者的意圖和功能點出發詳細介紹了Settings以及關聯的packages.xml等檔案在PKMS中的作用,從而為本篇PKMS啟動BOOT_PROGRESS_PMS_START階段分析打下了夯實的基礎,根據我們在前面的博客知道PKMS啟動可以劃分為好幾個階段,在今天的博客中我們將會重點從原始碼角度出發來分析它的BOOT_PROGRESS_PMS_START啟動階段相關邏輯和重點知識點,
通過前面博客PackageManagerService啟動詳解(一)之整體流程分析我們知道該階段執行的邏輯并不是非常的復雜但是卻非常繁瑣,對于PKMS其中的一些成員變數的初始化我們可以一筆帶過,但是其中最最需要我們注意的就是構建Settings大管家決議packages.xml檔案(當然第一次開機排除在外,因為此時相關的檔案還沒有創建和寫入成功),以及構建SystemConfig實體獲取系統配置資訊,譬如共享庫,系統feather,權限等,
并且上述的處理程序中牽涉到了非常多的原始碼,我只會截取其中的重點關鍵流程放出來,因為如果全對原始碼進行決議不僅會使讀者失去興趣,同時也會使本篇失去重點,當然如果讀者對原始碼的分析非常感興趣的話,墻裂建議參見博客PMS 第 2 篇,前提是你有毅力能看完,
事實上,學習是一個越往后學知道地越多的一個程序,一開始接觸一個新事物必然會有無數的疑問,但很多疑問都是邊邊角角的問題,并不會影響主流程的打通,最好的方式是先記錄下來,不求甚解,以后時不時拿出來審視一番,說不定在學到某個知識點的時候豁然開朗:哦,原來是這么一回事,
如果一開始就把寶貴的精力花費在深挖細枝末節的東西上,由于知識貯備不夠,問題必然會越挖越多,有的根本無從下手,有的甚至是無解的問題,進入一個死胡同,一個深淵巨坑,永遠走不出來跳不出來,
注意:本篇的介紹是基于Android 7.xx平臺為基礎的(并且為了書寫簡便后續PackageManagerService統一簡稱為PKMS),其中涉及的代碼路徑如下:
--- frameworks/base/services/core/java/com/android/server/pm/
PackageSetting.java
Settings.java
PackageSettingBase.java
PackageSignatures.java
SettingBase.java
SharedUserSetting.java
UserManagerService.java
--- frameworks/base/core/java/com/android/server/SystemConfig.java
--- frameworks/base/services/java/com/android/server/SystemServer.java
--- frameworks/base/core/java/android/os/Process.java
--- frameworks/base/core/jni/android_util_Process.cpp
--- system/core/include/private/android_filesystem_config.h
一.PKMS啟動BOOT_PROGRESS_PMS_START前奏階段
在BOOT_PROGRESS_PMS_START階段,牽涉的邏輯并不是非常的復雜,但是其原始碼邏輯卻是非常非常的多,所以這里為了篇幅的排版和美觀我會將BOOT_PROGRESS_PMS_START階段又劃分為四個小階段(這里是我人為劃分的,實際上并不存在這三個階段之說):
1.前奏
2.發展
3.高潮(不是那個,暫時沒有找到比較好的詞來描述)
4.結尾
等四個小階段來對上述流程展開分析,
至此我們正式開始第一階段來分析,由于原始碼篇幅很長,我們先來個總體的階段的回顧:
public PackageManagerService(Context context, Installer installer,
boolean factoryTest, boolean onlyCore) {
/************************* PKMS啟動第一階段 *************************/
//寫入日志,標識PackageManagerService正式開始第一階段的啟動
EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_START,
SystemClock.uptimeMillis());
//SDK版本檢測
if (mSdkVersion <= 0) {
Slog.w(TAG, "**** ro.build.version.sdk not set!");
}
mContext = context;
mPermissionReviewRequired = context.getResources().getBoolean(
R.bool.config_permissionReviewRequired);
//開機模式是否為工廠模式,默認為false
mFactoryTest = factoryTest;
mOnlyCore = onlyCore;//默認為false,標記是否只加載核心服務
// 用于存盤與顯示屏相關的一些屬性,例如螢屏的寬 / 高尺寸,解析度等資訊
mMetrics = new DisplayMetrics();
/******************** BOOT_PROGRESS_PMS_START前奏階段開始 **************************/
/*
在Settings中,創建packages.xml、packages-backup.xml、packages.list 等檔案物件
這個Settings物件非常重要,我們在后續的分析中會多次看到并使用到它
它是Android系統已經安裝Package在記憶體中的資料映射,存盤了譬如已安裝應用的代碼位置,資料位置,簽名等資訊
*/
mSettings = new Settings(mPackages);// 【 1.1 】
/*
向Settings實體物件添加system, phone, log, nfc, bluetooth, shell這六種shareUserId到mSettings中,
后面掃描和安裝有用!
其中的system對應的shareUserId也就是我們經常所說的對應的Android系統權限
*/
mSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);// 【 1.2 】
mSettings.addSharedUserLPw("android.uid.phone", RADIO_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw("android.uid.log", LOG_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw("android.uid.nfc", NFC_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw("android.uid.bluetooth", BLUETOOTH_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw("android.uid.shell", SHELL_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
//此處具體用途不是很清晰,忽略
File setFile = new File(AlarmManager.POWER_OFF_ALARM_SET_FILE);
File handleFile = new File(AlarmManager.POWER_OFF_ALARM_HANDLE_FILE);
mIsAlarmBoot = SystemProperties.getBoolean("ro.alarm_boot", false);
if (mIsAlarmBoot) {
...
} else if (setFile.exists() && handleFile.exists()) {
...
}
//這塊具體用途不是很清晰,應該是和除錯行程隔離有關
String separateProcesses = SystemProperties.get("debug.separate_processes");
if (separateProcesses != null && separateProcesses.length() > 0) {
if ("*".equals(separateProcesses)) {
mDefParseFlags = PackageParser.PARSE_IGNORE_PROCESSES;
mSeparateProcesses = null;
Slog.w(TAG, "Running with debug.separate_processes: * (ALL)");
} else {
mDefParseFlags = 0;
mSeparateProcesses = separateProcesses.split(",");
Slog.w(TAG, "Running with debug.separate_processes: "
+ separateProcesses);
}
} else {
mDefParseFlags = 0;
mSeparateProcesses = null;
}
/*
installer在SystemServer中被構造,這里通過該物件與底層installd進行socket通信
進行具體安裝與卸載的操作
*/
mInstaller = installer;
//創建PackageDexOptimizer,該類用于輔助進行dex優化
mPackageDexOptimizer = new PackageDexOptimizer(installer, mInstallLock, context,
"*dexopt*");
mMoveCallbacks = new MoveCallbacks(FgThread.get().getLooper());
//創建OnPermissionChangeListeners物件,用于監聽權限改變!
mOnPermissionChangeListeners = new OnPermissionChangeListeners(
FgThread.get().getLooper());
getDefaultDisplayMetrics(context, mMetrics);
/******************** BOOT_PROGRESS_PMS_START發展階段開始 **************************/
/*
構建SystemConfig物件實體(單例模式)
它主要用于獲取系統配置資訊
譬如共享庫,系統feather,權限等
*/
SystemConfig systemConfig = SystemConfig.getInstance(); // 【2.1】
mGlobalGids = systemConfig.getGlobalGids(); // 【2.7】
mSystemPermissions = systemConfig.getSystemPermissions();
mAvailableFeatures = systemConfig.getAvailableFeatures();
mProtectedPackages = new ProtectedPackages(mContext);
synchronized (mInstallLock) {
// writer
synchronized (mPackages) {
/*
構建并啟動Handler處理執行緒,用于處理應用的安裝和卸載相關事件的處理
這里為啥要單獨開辟一個執行緒處理呢,這是因為第三方應用的安裝和卸載是一個比較耗時的操作
*/
mHandlerThread = new ServiceThread(TAG,
Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
mHandlerThread.start();
//創建一個 PackageHandler 物件,系結前面創建的HandlerThread,處理安裝和卸載
mHandler = new PackageHandler(mHandlerThread.getLooper()); // 【 2.9 】
mProcessLoggingHandler = new ProcessLoggingHandler();
//使用看門狗檢測當前訊息處理執行緒,如果超時則觸發看門狗機制
Watchdog.getInstance().addThread(mHandler, WATCHDOG_TIMEOUT);
//創建默認權限管理物件,用于給某些預制的 apk 給予權限,也可以撤銷!
mDefaultPermissionPolicy = new DefaultPermissionGrantPolicy(this);
/*
創建需要掃描檢測的目錄檔案物件,為后續掃描做準備!
這里最最常見的就是/data/app/和/data/app-private/目錄
*/
File dataDir = Environment.getDataDirectory();
mAppInstallDir = new File(dataDir, "app");
mAppLib32InstallDir = new File(dataDir, "app-lib");
mEphemeralInstallDir = new File(dataDir, "app-ephemeral");
mAsecInternalPath = new File(dataDir, "app-asec").getPath();
mDrmAppPrivateInstallDir = new File(dataDir, "app-private");
mRegionalizationAppInstallDir = new File(dataDir, "app-regional");
//構造UserManagerService物件,創建用戶管理服務
sUserManager = new UserManagerService(context, this, mPackages);
//讀取權限組態檔中的資訊,保存到mPermissions這個ArrayMap中
ArrayMap<String, SystemConfig.PermissionEntry> permConfig
= systemConfig.getPermissions(); // 【2.7】
for (int i=0; i<permConfig.size(); i++) {
SystemConfig.PermissionEntry perm = permConfig.valueAt(i);
BasePermission bp = mSettings.mPermissions.get(perm.name);
// 加入到Settings的mPermissions中,SystemConfig決議的權限的包名都為android
if (bp == null) {
// BasePermission.TYPE_BUILTIN表示的是在編譯時期就確定好的,系統要提供的權限!
bp = new BasePermission(perm.name, "android", BasePermission.TYPE_BUILTIN);
mSettings.mPermissions.put(perm.name, bp);
}
if (perm.gids != null) {
// 如果系統權限有所屬的gids,將其添加到BasePermission物件中
bp.setGids(perm.gids, perm.perUser);
}
}
//獲取并處理所有共享庫資訊
ArrayMap<String, String> libConfig = systemConfig.getSharedLibraries(); // 【2.7】
for (int i=0; i<libConfig.size(); i++) {
mSharedLibraries.put(libConfig.keyAt(i),
new SharedLibraryEntry(libConfig.valueAt(i), null));
}
/******************** BOOT_PROGRESS_PMS_START高潮階段開始 **************************/
//嘗試讀取mac_permissions.xml中的selinux資訊
mFoundPolicyFile = SELinuxMMAC.readInstallPolicy();
/*
讀取檔案package.xml的內容,決議后存入mSettings的mPackages中
并且根據決議的結果判斷是否是第一次啟動,如果是第一次開機,那么是沒有相關資料的
*/
mFirstBoot = !mSettings.readLPw(sUserManager.getUsers(false));
/*
移除哪些codePath無效的Package(該Pacakge是系統App升級過來的被安裝在/data/app磁區)
此時需要恢復處于system目錄下的同名package
*/
final int packageSettingCount = mSettings.mPackages.size();
for (int i = packageSettingCount - 1; i >= 0; i--) {
PackageSetting ps = mSettings.mPackages.valueAt(i);
if (!isExternal(ps) && (ps.codePath == null || !ps.codePath.exists())
&& mSettings.getDisabledSystemPkgLPr(ps.name) != null) {
mSettings.mPackages.removeAt(i);
mSettings.enableSystemPackageLPw(ps.name);
}
}
if (mFirstBoot) {
/*
如果是第一次開機,從另外一個系統拷貝 odex 檔案到當前系統的 data 磁區
Android 7.1 引進了 AB 升級,這個是 AB 升級的特性,可以先不看!
*/
requestCopyPreoptedFiles();
}
//判斷是否自定義的決議界面(存在多個滿足添加的Activiyt,彈出的選擇界面的那個)
String customResolverActivity = Resources.getSystem().getString(
R.string.config_customResolverActivity);
if (TextUtils.isEmpty(customResolverActivity)) {
customResolverActivity = null;
} else {
mCustomResolverComponentName = ComponentName.unflattenFromString(
customResolverActivity);
}
//獲取掃描開始的時間
long startTime = SystemClock.uptimeMillis();
...
下面我們對該階段中涉及的重點流程逐一分析:
1.1 Settings大管家的構建
在正式開始分析Settings的構建之前,我們先來看下它涉及的類圖關系如下:

關于Settings牽涉的類以及關聯,可以詳見前面的博客PackageManagerService啟動詳解(二)之對已安裝應用怎么進行持久化存盤管理?,在這里我們要分析的是它們之間的關聯在原始碼層是怎么被構建的,并且墻裂建議讀者在開始原始碼分析前,將Settings涉及的幾個重要成員變數要給提前熟悉!
啥也不多說了,直接上原始碼:
//[Settings.java]
Settings(Object lock) {
// 這里傳入的lock物件是PMKS的mPackags
this(Environment.getDataDirectory(), lock);
}
Settings(File dataDir, Object lock) {
mLock = lock;
// 構建mRuntimePermissionsPersistence實體物件,用于處理運行時權限相關的操作
mRuntimePermissionsPersistence = new RuntimePermissionPersistence(mLock);
// 如果/data/system目錄沒有創建則創建,并設定該目錄對應的權限
mSystemDir = new File(dataDir, "system");
mSystemDir.mkdirs();
FileUtils.setPermissions(mSystemDir.toString(),
FileUtils.S_IRWXU|FileUtils.S_IRWXG
|FileUtils.S_IROTH|FileUtils.S_IXOTH,
-1, -1);
// 創建 packages.xml 和 packages-backup.xml 檔案物件
mSettingsFilename = new File(mSystemDir, "packages.xml");
mBackupSettingsFilename = new File(mSystemDir, "packages-backup.xml");
// 創建 packages.list 檔案物件,并設定權限資訊!
mPackageListFilename = new File(mSystemDir, "packages.list");
FileUtils.setPermissions(mPackageListFilename, 0640, SYSTEM_UID, PACKAGE_INFO_GID);
FileUtils.setPermissions(mSettingsFilename, 0640, SYSTEM_UID, SYSTEM_UID);
// 創建 sdcardfs 檔案物件!
final File kernelDir = new File("/config/sdcardfs");
mKernelMappingFilename = kernelDir.exists() ? kernelDir : null;
// Deprecated: Needed for migration
// 創建 packages-stopped.xml 和 packages-stopped-backup.xml 檔案物件!
mStoppedPackagesFilename = new File(mSystemDir, "packages-stopped.xml");
mBackupStoppedPackagesFilename = new File(mSystemDir, "packages-stopped-backup.xml");
}
這里我們可以看到Setings的構建比較簡單,主要就是構建了mRuntimePermissionsPersistence運行時權限管理物件,并將應用安裝相關的持久化資訊檔案加載到具體的File檔案類中,
關于/data/system/package*.xxx檔案這里就不過展開了,可以到PackageManagerService啟動詳解(二)之對已安裝應用怎么進行持久化存盤管理?中進行細看,(為了節奏的連貫性,我還是簡單介紹一下)
- packages.xml:記錄了系統中所有安裝的應用資訊,包括基本資訊、簽名和權限,這個是Android應用資訊持久化的關鍵檔案
- pakcages-back.xml:packages.xml檔案的備份,用于描述系統中所安裝的所有 Package 資訊,PMS 會先把資料寫入 packages-backup.xml,資訊寫成功后,再改名為 packages.xml
- pakcages-stoped.xml:記錄系統中被強制停止的運行的應用資訊,系統在強制停止某個應- 用的時候,會將應用的資訊記錄在該檔案中
- pakcages-stoped-backup.xml:pakcages-stoped.xml檔案的備份
- package-usage.list:記錄了應用的使用情況,譬如使用了多久
- packages.list:記錄了應用的一些簡單情況
1.2 Settings.addSharedUserLPw添加共享用戶
在Settings中部分方法帶有LP后綴,表示需要獲取mPackages這個鎖才能執行,
可以看到在PKMS中構建Settings成功以后,接著它也是毫不手軟的一口氣呼叫了五次addSharedUserLPw 方法添加了五個系統共享用戶(一夜五次郎),在分析它的邏輯之前,我們先來看下它傳入的引數:
-
String name:共享用戶名,下面是傳入的用戶名:
- android.uid.system
- android.uid.phone
- android.uid.log
- android.uid.nfc
- android.uid.bluetooth
- android.uid.shell
對于上面的共享用戶名,做ROM開發的最最熟悉,也最最經常用的肯定是android.uid.system了,
-
int uid:共享用 id,下面的用戶名和上面的id 一一對應!
- Process.SYSTEM_UID
- Process.RADIO_UID
- Process.LOG_UID
- Process.NFC_UID
- Process.BLUETOOTH_UID
- Process.SHELL_UID
關于共享id的具體描述,可以參見Process.java中,
-
int pkgFlags:
ApplicationInfo.FLAG_SYSTEM -
int pkgPrivateFlags:
ApplicationInfo.PRIVATE_FLAG_PRIVILEGED
好了入參我們整清楚了,直接來看原始碼,如下:
// [Settings.java]
SharedUserSetting addSharedUserLPw(String name, int uid, int pkgFlags, int pkgPrivateFlags) {
/*
從mSharedUsers串列中看能否根據用戶id取出系統共享用戶資訊,判斷當前系統共享用戶是否是重復添加
*/
SharedUserSetting s = mSharedUsers.get(name);
if (s != null) {
if (s.userId == uid) {//判斷uid是否發生變化
return s;
}
PackageManagerService.reportSettingsProblem(Log.ERROR,
"Adding duplicate shared user, keeping first: " + name);
return null;
}
// 創建 SharedUserSetting物件
s = new SharedUserSetting(name, pkgFlags, pkgPrivateFlags); // 【1.2.1】
s.userId = uid;
// 根據uid的范圍,保存到到mUserIds或mOtherUserIds中!
if (addUserIdLPw(uid, s, name)) { // 【1.2.2】
// 將前面創建的SharedUserSetting以name為key添加到mSharedUsers哈希串列中
mSharedUsers.put(name, s);
return s;
}
return null;
}
其主要邏輯分為如下:
- 根據傳入引數構建SharedUserSetting共享用戶類
- 將前面創建的SharedUserSetting實體,繼續呼叫addUserIdLPw進行下一步處理
- 然后根據條件判斷是否將前面創建的SharedUserSetting實體添加到mSharedUsers資料結構中
下面我們簡單對上述邏輯展開分析!
1.2.1 SharedUserSetting類
通過前面的博客我們知道,SharedUserSetting被設計的用途主要用來描述具有相同的sharedUserId的應用資訊,它的成員變數packages保存了所有具有相同sharedUserId的應用資訊參考,而成員變數userId則是記錄多個APK共享的UID,共享用戶的應用的簽名是相同的,簽名保存在成員變數signatures中(這里有一點需要注意,由于簽名相同,Android運行時很容易檢索到某個應用擁有相同的sharedUserId的其他應用),
在正式開始SharedUserSetting的原始碼前,我們先來看看它的類圖(至于它的關系圖,參見前面),如下:

好了,讓我們通過原始碼來揭開SharedUserSetting的真實面紗
// [SharedUserSetting.java]
final class SharedUserSetting extends SettingBase {
// 共享uid的名稱
final String name;
// uid的值
int userId;
// 和這個sharedUserId相關的flags
int uidFlags; // ApplicationInfo.FLAG_SYSTEM
int uidPrivateFlags; // ApplicationInfo.PRIVATE_FLAG_PRIVILEGED
// 使用這個共享uid的所有應用程式package資訊
final ArraySet<PackageSetting> packages = new ArraySet<PackageSetting>();
// 使用這個共享uid的簽名
final PackageSignatures signatures = new PackageSignatures();
SharedUserSetting(String _name, int _pkgFlags, int _pkgPrivateFlags) {
super(_pkgFlags, _pkgPrivateFlags);
uidFlags = _pkgFlags;
uidPrivateFlags = _pkgPrivateFlags;
name = _name;
}
@Override
public String toString() {
return "SharedUserSetting{" + Integer.toHexString(System.identityHashCode(this)) + " "
+ name + "/" + userId + "}";
}
...
}
SharedUserSetting類并不是非常的復雜,在現階段我們只需要了解它的幾個核心成員的意義,以及它和我們后續要說的PackageSetting有一個共同的爸比SettingBase即可,
1.2.2 Settings.addUserIdLPw添加userid相關資訊
我們繼續回到章節1.2中,繼續往下看Settings.addUserIdLPw方法的呼叫,該方法主要用于將前面創建好的SharedUserSetting實體物件根據其uid是系統uid還是非系統uid,添加到特定的集合中,其處理邏輯如下:
但是這里需要注意的是,這里只是在此種原始碼情況下obj引數特指SharedUserSetting物件,在后續的分析中也會呼叫到addUserIdLPw方法,它傳入的引數obj就不一定是SharedUserSetting也可能是PackageSetting了
//[Settings.java]
// 存盤所有package.xml中shared-user標簽的資訊
// 是一個以String型別的name(比如"android.uid.system")為"key",以SharedUserSetting物件為"value"的HashMap
final ArrayMap<String, SharedUserSetting> mSharedUsers =
new ArrayMap<String, SharedUserSetting>();
// 存盤是非系統應用的uid相關資訊,包括共享和非共享
private final ArrayList<Object> mUserIds = new ArrayList<Object>();
// 存盤是系統應用的的的uid相關資訊,包括共享和非共享
private final SparseArray<Object> mOtherUserIds =
new SparseArray<Object>();
/*
addUserIdLPw將創建的SharedUserSetting物件根據其uid是系統uid還是非系統uid
添加到指定的集合中!
但是這里需要注意的是,這里只是在這種情況下特指SharedUserSetting物件,在后續的分析中
也會呼叫到addUserIdLPw方法,它傳入的引數obj就不一定是SharedUserSetting也可能是PackageSetting了
*/
private boolean addUserIdLPw(int uid, Object obj, Object name) {
// uid不能超過限制
if (uid > Process.LAST_APPLICATION_UID) {
return false;
}
// 如果uid屬于非系統應用,將其加入到mUserIds集合中
if (uid >= Process.FIRST_APPLICATION_UID) {
int N = mUserIds.size();
final int index = uid - Process.FIRST_APPLICATION_UID;
while (index >= N) {
mUserIds.add(null);
N++;
}
if (mUserIds.get(index) != null) {
PackageManagerService.reportSettingsProblem(Log.ERROR,
"Adding duplicate user id: " + uid
+ " name=" + name);
return false;
}
mUserIds.set(index, obj);
} else {
// 如果uid屬于系統應用,將其加入到mOtherUserIds集合中!
if (mOtherUserIds.get(uid) != null) {
PackageManagerService.reportSettingsProblem(Log.ERROR,
"Adding duplicate shared id: " + uid
+ " name=" + name);
return false;
}
mOtherUserIds.put(uid, obj);
}
return true;
}
這里的addUserIdLPw的處理邏輯比較簡單,我們就不過多的贅述了,但是其中涉及的幾個變數,我們簡單說明一下:
- Process.FIRST_APPLICATION_UID:它的值被定義成10000,用來區分當前的uid是屬于系統應用還是非系統應用的,非系統應用的uid的取值在10000到19999,系統應用的uid小10000;
- Process.LAST_APPLICATION_UID:它的值被定義為19999,表示應用程式的uid最大的合法取值為19999,如果超過這個值表明uid非法了
在本篇博客此處和后續的多處,會很頻繁的提及到uid的概念,這個我覺得還是有必要重點強調一下關于Android中uid和pid兩個的概念!
- PID:為Process Identifier的簡稱, PID就是各行程的身份標識,程式一運行系統就會自動分配給行程一個獨一無二的PID,行程中止后PID被系統回收,可能會被繼續分配給新運行的程式,但是在android系統中一般不會把已經kill掉的行程ID重新分配給新的行程,新產生行程的行程號,一般比產生之前所有的行程號都要大,
- UID:一般理解為User Identifier,UID在linux中就是用戶的ID,表明時哪個用戶運行了這個程式,主要用于權限的管理,而在android 中又有所不同,因為android為單用戶系統,這時UID 便被賦予了新的使命,資料共享,為了實作資料共享,android為每個應用幾乎都分配了不同的UID,不像傳統的linux,每個用戶相同就為之分配相同的UID,(當然這也就表明了一個問題,android只能時單用戶系統,在設計之初就被他們的工程師給閹割了多用戶),使之成了資料共享的工具,
因此在android中PID,和UID都是用來識別應用程式的身份的,但UID是為了不同的程式來使用共享的資料而使用的
1.3 PKMS啟動BOOT_PROGRESS_PMS_START前奏小結
至此PKMS啟動BOOT_PROGRESS_PMS_START前奏階段核心的內容分析到這里就告一段了,其中的一些其他的成員變數類的初始化,我就沒有重點表明出來了(感興趣的讀者,可以自行分析),這里我們還是對該階段簡單總結一下:
- 首先創建Settings實體物件,該實體物件非常重要,它主要用來記錄上次Android終端已安裝應用相關資訊的一個管理類,如果Android終端設備是第一次啟動(如果沒有發生例外的情況)則會創建packages.xml、packages-backup.xml、packages.list等檔案,同時添加 system, phone, log, nfc, bluetooth, shell這六種 shareUserId 到mSettings中進行管理,該物件被初始化成功之后,后面的掃描安裝都會被用到,如果應用的情況(即有新的應用安裝,卸載,升級)發生變化,該物件也會進行相應的更新
- 初始mInstaller物件,Installer是一個系統服務,它封裝了很多PMS進行包管理需要用到的方法,譬如install()、 dexopt()、rename()等,在Installer內部,其實是通過Socket連接installd,將執行指令發送到installd完成具體的操作
- 創建PackageDexOptimizer物件,該物件主要用于對應用進行相關的odex優化操作,它是進行Dex優化的工具類,對于一個APK而言,編譯后其APK包中可執行檔案的格式dex,安裝到了手機上以后,需要經過檔案格式轉化才能運行,譬如APK需要轉換成oat格式才能在ART虛擬機上運行,檔案格式轉換的程序就叫DexOpt,DalvikVM的時代,Android可執行檔案的格式是dex,有一種進一步優化的格式叫odex;ART虛擬機的時代,Android可執行檔案的格式是oat,雖然都叫做DexOpt,但在DalvikVM和ART兩種不同虛擬機的時代分別有不同的內涵
- 創建OnPermissionChangeListeners物件,該物件主要用于監聽用戶uid權限改變,當監聽到用戶uid權限發生變化之后,會將相關資訊發送給注冊好的監聽者,是一個典型的觀察者設計模式
二.PKMS啟動BOOT_PROGRESS_PMS_START發展階段
好了,前面階段分析完畢,我們開始下一階段的分析,在這一階段中我們的重中之重是SystemConfig的,首先我們會從整體上開始介紹,然后再從原始碼入手,
2.1 SystemConfig類簡介
SystemConfig是用來加載并存盤系統全域的配置資訊的資料結構,其中涉及的資料有系統配置資訊,權限資訊,gid,共享庫等資訊,其原始的資料來源于/system/etc/sysconfig和/system/etc/permissions目錄下的XML檔案,在SystemConfig物件構建時,會讀取這兩個目錄下所有XML檔案的內容,其中的XML檔案主要從如下幾個維度來進行描述:
-
權限與gid的映射關系:譬如,以下內容表示屬于inet這個組的用戶都擁有android.permission.INTERNET權限:
<permission name="android.permission.INTERNET" > <group git="inet"> </permission> -
權限與uid的映射關系:譬如,以下內容表示uid為meida用戶擁有android.permission.CAMERA這個權限:
<assign-permission name="android.permission.CAMERA" uid="media" /> -
共享庫的定義:在Android中有很多公共庫,除了BOOTCLASSPATH和SYSTEMSERVERCLASSPATH中定義的之外,框架層還支持額外的擴展,譬如,以下內容表示公共庫的包名和其路徑的關系:
<library name="org.apache.http.legacy" file="/system/framework/org.apache.http.legacy.jar" />
這里我們有了一個基本的了解,下面就從SystemConfig的構造開始分析,
2.2 SystemConfig的構建
通過getInstance獲取的方式來構建SystemConfig實體,我們很明顯的可以知道SystemConfig是一個單例模式的,我們直接入手來看看:
// [SystemConfig.java]
public static SystemConfig getInstance() {
synchronized (SystemConfig.class) {
if (sInstance == null) {
sInstance = new SystemConfig();
}
return sInstance;
}
}
插播一條廣告,插入一條SystemConfig類圖

好了,我們繼續往下看SystemConfig的構造方法:
// [SystemConfig.java]
/*
在構造器中會呼叫相關方法讀取配置資訊
*/
SystemConfig() {
// 讀取/etc/sysconfig/目錄下的配置資訊
readPermissions(Environment.buildPath(
Environment.getRootDirectory(), "etc", "sysconfig"), ALLOW_ALL);
// 讀取/etc/permissions/目錄下的配置資訊
readPermissions(Environment.buildPath(
Environment.getRootDirectory(), "etc", "permissions"), ALLOW_ALL); // 【2.3】
int odmPermissionFlag = ALLOW_LIBS | ALLOW_FEATURES | ALLOW_APP_CONFIGS;
readPermissions(Environment.buildPath(
Environment.getOdmDirectory(), "etc", "sysconfig"), odmPermissionFlag);
readPermissions(Environment.buildPath(
Environment.getOdmDirectory(), "etc", "permissions"), odmPermissionFlag);
readPermissions(Environment.buildPath(
Environment.getOemDirectory(), "etc", "sysconfig"), ALLOW_FEATURES);
readPermissions(Environment.buildPath(
Environment.getOemDirectory(), "etc", "permissions"), ALLOW_FEATURES);
...
}
SystemConfig構造方法核心邏輯是呼叫readPermissions方法讀取相關目錄下的組態檔資訊,這里我們可以看到readPermissions方法有兩個入參引數,在進行下面的介紹前我們簡單對它的兩個引數簡單介紹一下:
-
File libraryDir:表示可以訪問的目錄,這些目錄包括:
/etc/sysconfig/ /etc/permissions/ /odm/sysconfig/ /odm/permissions/ /oem/sysconfig/ /oem/permissions/這里我們以/etc/permissions/為例,可以看到該目錄下面有一堆的xml檔案!

-
int permissionFlag:表示目錄被設定對應的flag時,可以有那些配置資訊的型別用來被讀取,關于該flag的定義如下:
// 這個沒有啥好注釋的了,見名思意 private static final int ALLOW_FEATURES = 0x01; private static final int ALLOW_LIBS = 0x02; private static final int ALLOW_PERMISSIONS = 0x04; private static final int ALLOW_APP_CONFIGS = 0x08; private static final int ALLOW_ALL = ~0;
這里我們將上述一番邏輯擺出來以后,經過一番轉換,就得到了如下的最終公式結論:
1. /oem/etc/sysconfig和/oem/etc/permissions目錄:只能設定 features!
2. /odm/etc/sysconfig和/odm/etc/permissions目錄:只能設定 libs,features 和 app configs!
3. /system/etc/sysconfig和/system/etc/permissions目錄:可以設定所有的配置如libs,permissions,features,app configs 等等!
2.3 SystemConfig類管理的組態檔格式簡介
在正式開始介紹SystemConfig呼叫readPermissions方法之前,我們非常有必要拿簡單介紹下它讀取的組態檔的格式,這里我們以//etc/permissions/下面的platform.xml和android.hardware.bluetooth.xml為例說明:
這里為了描述方便,我會將上述兩個xml中的<permissions>根標簽的子標簽都放在一起,這樣的排版比較好(注意readPermissions讀取的配置xml檔案的根標簽必須是<permissions>或者<config>這兩種),
并且在本篇博客中我們只會重點分析permission,assign-permission,library標簽的決議,其它的標簽感興趣的讀者可以自行研究,
<permissions>
<!-- 權限與gid的映射關系,譬如,以下內容表示屬于net_bt這個組的用戶都擁有android.permission.BLUETOOTH權限
并且這里有一點需要注意的是,這里group可以有多個 -->
<permission name="android.permission.BLUETOOTH" >
<group gid="net_bt" />
</permission>
...
<!-- 權限與uid的映射關系,譬如,以下內容表示uid為meida用戶擁有android.permission.WAKE_LOCK這個權限 -->
<assign-permission name="android.permission.WAKE_LOCK" uid="media" />
...
<!-- 擴展的Android共享庫(注意指的是jar包,而不是so型別的共享庫),以下內容表示共享庫的包名和其路徑的關系-->
<library name="android.test.runner"
file="/system/framework/android.test.runner.jar" />
...
<!-- 該子標簽是被定義在android.hardware.bluetooth.xml中的,表明當前設備終端是否支持一些feature特性,
我們通常通過hasSystemFeature來進行判斷,譬如我們最最常見的就是在SystemServer.java中根據feature特性
來決定是否啟動一些和硬體特性關聯的服務,譬如BLE,NFC等 -->
<feature name="android.hardware.bluetooth" />
...
<!-- 下面的子標簽感興趣的讀者可以自行研究,這里不在當前博客重點討論的范圍之內 -->
<!-- These are the standard packages that are white-listed to always have internet
access while in power save mode, even if they aren't in the foreground. -->
<allow-in-power-save package="com.android.shell" />
<!-- These are the standard packages that are white-listed to always have internet
access while in data mode, even if they aren't in the foreground. -->
<allow-in-data-usage-save package="com.android.providers.downloads" />
<!-- These are the packages that are white-listed to be able to run as system user -->
<system-user-whitelisted-app package="com.android.settings" />
<!-- These are the packages that shouldn't run as system user -->
<system-user-blacklisted-app package="com.android.wallpaper.livepicker" />
</permissions>
同時在后續的具體標簽決議中,我們也會以上面我所例舉的xml檔案為模板進行相關的決議,所以讀者有必要提前了解一下,
2.4 SystemConfig.readPermissions處理系統配置目錄
好了,要決議的檔案格式和組成我們了解清楚了,讓我們從心出發,錯了從原始碼出發出發來看readPermissions是怎么決議組態檔的,
// 【SystemConfig.java】
void readPermissions(File libraryDir, int permissionFlag) {
// 檢測目錄可讀性
if (!libraryDir.exists() || !libraryDir.isDirectory()) {
if (permissionFlag == ALLOW_ALL) {
Slog.w(TAG, "No directory " + libraryDir + ", skipping");
}
return;
}
if (!libraryDir.canRead()) {
Slog.w(TAG, "Directory " + libraryDir + " cannot be read");
return;
}
// 遍歷決議目錄下的所有*.xml 檔案!
File platformFile = null;
for (File f : libraryDir.listFiles()) {
// 對于/etc/permissions/platform.xml檔案,最后再處理!
if (f.getPath().endsWith("/etc/permissions/platform.xml")) {
platformFile = f;
continue;
}
// 例外處理
if (!f.getPath().endsWith(".xml")) {
Slog.i(TAG, "Non-xml file " + f + " in " + libraryDir + " directory, ignoring");
continue;
}
if (!f.canRead()) {
Slog.w(TAG, "Permissions library file " + f + " cannot be read");
continue;
}
// 進一步決議檔案
readPermissionsFromXml(f, permissionFlag); 【 2.5 】
}
if (platformFile != null) {
// 單獨決議platform.xml檔案
readPermissionsFromXml(platformFile, permissionFlag);
}
}
readPermissions的處理邏輯比較簡單,其流程如下:
- 先做一些常規檢測,譬如檢測配置目錄權限,可讀性等
- 呼叫readPermissionsFromXml方法解決配置目錄檔案下的xml檔案,但是對/etc/permissions/platform.xml的檔案單獨做特殊處理,放到最后才進行決議(當然前提得是有才可以)
這里我們看到,最后無論是那個xml檔案都殊途同歸的呼叫了readPermissionsFromXml方法來進行下一步的處理,那么我們還有什么理由不跟進去一探究竟呢!
2.5 SystemConfig.readPermissionsFromXml決議系統組態檔
我們接著繼續來看readPermissionsFromXml處理具體組態檔的流程,通過前面可知組態檔中的標簽有很多,這里我們只會決議前面2.2章節重點表明的標簽,其它的感興趣的讀者可以嘗試自行處理!
// 【 SystemConfig.java】
/*
這個方法洋洋灑灑好幾百行,但是核心思想只有一個就是決議xml中的各種標簽
*/
private void readPermissionsFromXml(File permFile, int permissionFlag) {
FileReader permReader = null;
try {
permReader = new FileReader(permFile);
} catch (FileNotFoundException e) {
Slog.w(TAG, "Couldn't find or open permissions file " + permFile);
return;
}
/*
判斷當前的Android設備是否是低記憶體設備
如果是低記憶體的設備,則android對一些feature特性則會進行放棄
*/
final boolean lowRam = ActivityManager.isLowRamDeviceStatic();
try {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(permReader);
int type;
while ((type=parser.next()) != parser.START_TAG
&& type != parser.END_DOCUMENT) {
;
}
if (type != parser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
// 判斷根標簽是否是permissions或者config,其它的都是非法的
if (!parser.getName().equals("permissions") && !parser.getName().equals("config")) {
throw new XmlPullParserException("Unexpected start tag in " + permFile
+ ": found " + parser.getName() + ", expected 'permissions' or 'config'");
}
/*
通過位與操作,判斷當前的目錄可以設定哪些權限
這種方式在Android中比較常見
*/
boolean allowAll = permissionFlag == ALLOW_ALL;
boolean allowLibs = (permissionFlag & ALLOW_LIBS) != 0;
boolean allowFeatures = (permissionFlag & ALLOW_FEATURES) != 0;
boolean allowPermissions = (permissionFlag & ALLOW_PERMISSIONS) != 0;
boolean allowAppConfigs = (permissionFlag & ALLOW_APP_CONFIGS) != 0;
while (true) {
// 決議下一個標簽
XmlUtils.nextElement(parser);
if (parser.getEventType() == XmlPullParser.END_DOCUMENT) {
break;
}
String name = parser.getName();
/*
根據標簽和決議規則決議"group"標簽
然后將決議得到的gid存放在mGlobalGids中
*/
if ("group".equals(name) && allowAll) { // 【 2.5.1 】
// 決議xml檔案中定義的gid資訊
String gidStr = parser.getAttributeValue(null, "gid");
if (gidStr != null) {
int gid = android.os.Process.getGidForName(gidStr);
// 將得到的gid添加到mGlobalGids串列中
mGlobalGids = appendInt(mGlobalGids, gid);
} else {
Slog.w(TAG, "<group> without gid in " + permFile + " at "
+ parser.getPositionDescription());
}
XmlUtils.skipCurrentTag(parser);
continue;
}
/*
根據標簽和決議規則決議"permission"標簽
*/
else if ("permission".equals(name) && allowPermissions) {
//決議系統中所有權限和其所屬gid的資訊
String perm = parser.getAttributeValue(null, "name");
if (perm == null) {
Slog.w(TAG, "<permission> without name in " + permFile + " at "
+ parser.getPositionDescription());
XmlUtils.skipCurrentTag(parser);
continue;
}
perm = perm.intern();
// 呼叫readPermission繼續決議權限
readPermission(parser, perm); // 【 2.5.2 】
}
/*
如果allowPermissions為true,決議"assign-permission"標簽
決議系統中uid和其所持有的權限的關系!
*/
else if ("assign-permission".equals(name) && allowPermissions) { // 【 2.5.3】
String perm = parser.getAttributeValue(null, "name");//權限名稱
if (perm == null) {
Slog.w(TAG, "<assign-permission> without name in " + permFile + " at "
+ parser.getPositionDescription());
XmlUtils.skipCurrentTag(parser);
continue;
}
String uidStr = parser.getAttributeValue(null, "uid");// 擁有該權限的uid
if (uidStr == null) {
Slog.w(TAG, "<assign-permission> without uid in " + permFile + " at "
+ parser.getPositionDescription());
XmlUtils.skipCurrentTag(parser);
continue;
}
int uid = Process.getUidForName(uidStr);// 將uid轉為init值
if (uid < 0) {
Slog.w(TAG, "<assign-permission> with unknown uid \""
+ uidStr + " in " + permFile + " at "
+ parser.getPositionDescription());
XmlUtils.skipCurrentTag(parser);
continue;
}
perm = perm.intern();
ArraySet<String> perms = mSystemPermissions.get(uid);
if (perms == null) {
perms = new ArraySet<String>();
mSystemPermissions.put(uid, perms);
}
perms.add(perm);
XmlUtils.skipCurrentTag(parser);
}
/*
如果 allowLibs為true,決議"library"標簽
獲得系統中所有共享庫的資訊!
*/
else if ("library".equals(name) && allowLibs) {
String lname = parser.getAttributeValue(null, "name");
String lfile = parser.getAttributeValue(null, "file");
if (lname == null) {
Slog.w(TAG, "<library> without name in " + permFile + " at "
+ parser.getPositionDescription());
} else if (lfile == null) {
Slog.w(TAG, "<library> without file in " + permFile + " at "
+ parser.getPositionDescription());
} else {
//Log.i(TAG, "Got library " + lname + " in " + lfile);
mSharedLibraries.put(lname, lfile);
}
XmlUtils.skipCurrentTag(parser);
continue;
}
/*
決議feather子標簽
*/
else if ("feature".equals(name) && allowFeatures) {
String fname = parser.getAttributeValue(null, "name");
int fversion = XmlUtils.readIntAttribute(parser, "version", 0);
boolean allowed;
if (!lowRam) {
allowed = true;
} else {
// 記憶體不足時,配置了 notLowRam 的 feature 不會在加載!
String notLowRam = parser.getAttributeValue(null, "notLowRam");
allowed = !"true".equals(notLowRam);
}
if (fname == null) {
Slog.w(TAG, "<feature> without name in " + permFile + " at "
+ parser.getPositionDescription());
} else if (allowed) {
// 將feature構造成featureInfo,加入到mAvailableFeatures物件中!
addFeature(fname, fversion);
}
XmlUtils.skipCurrentTag(parser);
continue;
}
/*
決議"unavailable-feature",如果有則將其添加到mUnavailableFeatures中
*/
else if ("unavailable-feature".equals(name) && allowFeatures) {
String fname = parser.getAttributeValue(null, "name");
if (fname == null) {
Slog.w(TAG, "<unavailable-feature> without name in " + permFile + " at "
+ parser.getPositionDescription());
} else {
mUnavailableFeatures.add(fname);
}
XmlUtils.skipCurrentTag(parser);
continue;
}
/*
用于獲得系統中處于省電模式白名單,而沒有在 idle 模式白名單中的應用資訊!
這些應用可以在后臺運行!
*/
else if ("allow-in-power-save-except-idle".equals(name) && allowAll) {
}
/*
決議 "allow-in-power-save" 標簽
獲得哪些處于 doze 模式白名單中的應用資訊!
*/
else if ("allow-in-power-save".equals(name) && allowAll) {
...
}
/*
決議 "allow-in-data-usage-save" 標簽,
獲得哪些處于流量節省模式白名單中的應用的資訊,在白名單中的應用,當系統處于
流量節省模式時,可以在后臺運行!
*/
else if ("allow-in-data-usage-save".equals(name) && allowAll) {
...
}
/*
決議app-link標簽
*/
else if ("app-link".equals(name) && allowAppConfigs) {
...
}
/*
決議system-user-whitelisted-app標簽
獲得哪些在system user下可以運行的應用資訊!
*/
else if ("system-user-whitelisted-app".equals(name) && allowAppConfigs) {
...
}
/*
決議system-user-blacklisted-app標簽
獲得哪些在system user下不可以運行的應用資訊!
*/
else if ("system-user-blacklisted-app".equals(name) && allowAppConfigs) {
...
} else if ("default-enabled-vr-app".equals(name) && allowAppConfigs) {
...
} else if ("backup-transport-whitelisted-service".equals(name) && allowFeatures) {
...
} else if ("disabled-until-used-preinstalled-carrier-associated-app".equals(name)
&& allowAppConfigs) {
...
} else {
...
}
}
} catch (XmlPullParserException e) {
Slog.w(TAG, "Got exception parsing permissions.", e);
} catch (IOException e) {
Slog.w(TAG, "Got exception parsing permissions.", e);
} finally {
IoUtils.closeQuietly(permReader);
}
// 處理加密相關的feature
if (StorageManager.isFileEncryptedNativeOnly()) {
addFeature(PackageManager.FEATURE_FILE_BASED_ENCRYPTION, 0);
addFeature(PackageManager.FEATURE_SECURELY_REMOVES_USERS, 0);
}
// 從mAvailableFeatures移除不支持的feature
for (String featureName : mUnavailableFeatures) {
removeFeature(featureName);
}
}
我們可以看到上述決議的標簽何其多,不過沒有關系,我們抓大放小(并不是說其它標簽的內容不重要,而是用的比較少),在接下來的博客中我們只會重點對如下的幾個標簽決議重點分析:
- group標簽
- permission”標簽
- assign-permission標簽
- library標簽
- feature標簽
- unavailable-feature標簽
其它標簽的決議套路也差不多,通過xml決議器得到標簽的資料,然后存放在一定的資料結構中,這里限于篇幅和抓大放小的原則,就先過了,
在正式開始決議相關標簽之前,我們先來看下SystemConfig中存放各種標簽的資料結構,如下:
// [ SystemConfig.java ]
public class SystemConfig {
// 存放從系統配置讀取的所有gid
int[] mGlobalGids;
// 保存了uid和其所有的權限資訊的映射關系
final SparseArray<ArraySet<String>> mSystemPermissions = new SparseArray<>();
// 系統所有的共享庫資訊,key是共享庫的名稱,value是共享庫的路徑
final ArrayMap<String, String> mSharedLibraries = new ArrayMap<>();
// 系統中可用的feature資訊
final ArrayMap<String, FeatureInfo> mAvailableFeatures = new ArrayMap<>();
// 系統中不可用的feature
final ArraySet<String> mUnavailableFeatures = new ArraySet<>();
public static final class PermissionEntry {
public final String name;// 權限名稱
public int[] gids; // 該權限所屬gid
public boolean perUser; //表示該權限的gid是否針對不同的userId做調整
PermissionEntry(String name, boolean perUser) {
this.name = name;
this.perUser = perUser;
}
}
// 保存了gid和其所擁有的權限的映射關系
final ArrayMap<String, PermissionEntry> mPermissions = new ArrayMap<>();
/*
其它,其它,其它
*/
final ArraySet<String> mAllowInPowerSaveExceptIdle = new ArraySet<>();
final ArraySet<String> mAllowInPowerSave = new ArraySet<>();
final ArraySet<String> mAllowInDataUsageSave = new ArraySet<>();
final ArraySet<String> mLinkedApps = new ArraySet<>();
final ArraySet<String> mSystemUserWhitelistedApps = new ArraySet<>();
final ArraySet<String> mSystemUserBlacklistedApps = new ArraySet<>();
final ArraySet<ComponentName> mDefaultVrComponents = new ArraySet<>();
final ArraySet<ComponentName> mBackupTransportWhitelist = new ArraySet<>();
final ArrayMap<String, List<String>> mDisabledUntilUsedPreinstalledCarrierAssociatedApps =
new ArrayMap<>();
2.5.1 決議group子標簽獲得系統定義的gid用戶組
通過前面章節2.3我們可知group相關的xml內容通用格式如下(這里我們只是隨意摘取了一個出來),可以看到group標簽并不最外層標簽的子標簽而存在的,而是和 permission一起配合使用的,如下:
<permissions>
<!-- 權限與gid的映射關系,譬如,以下內容表示屬于net_bt這個組的用戶都擁有android.permission.BLUETOOTH權限
并且這里有一點需要注意的是,這里group可以有多個 -->
<permission name="android.permission.BLUETOOTH" >
<group gid="net_bt" />
</permission>
<permissions、>
我們直接看下它的決議程序,如下:
// 根據標簽和決議規則決議"group"標簽
if ("group".equals(name) && allowAll) {
// 決議xml檔案中定義的gid資訊
String gidStr = parser.getAttributeValue(null, "gid");
if (gidStr != null) {
int gid = android.os.Process.getGidForName(gidStr);
mGlobalGids = appendInt(mGlobalGids, gid);
} else {
Slog.w(TAG, "<group> without gid in " + permFile + " at "
+ parser.getPositionDescription());
}
XmlUtils.skipCurrentTag(parser);
continue;
}
我們可以看到根據組字串gid的名稱,通過呼叫android.os.Process.getGidForName轉為int型別的gid,保存到 mGlobalGids陣列中,并且這里的getGidForName是一個本地方法,最后會呼叫到android_util_Process.cpp函式中,
這里補充一個知識點,可以看到在這里定義了如果的group,那么在我們的Android終端中要怎么具體查看呢,我們可以通過終端中執行groups命令來進行(詳情讀者可以參見博客linux如何查看所有的用戶和組資訊?),如下:
可以看到我們決議的net_bt赫然在列,并且當前我是root用戶登錄的,所以不要驚訝沒有看到system和shell用戶組!
至于Android支持那些用戶組,詳見system/core/include/private/android_filesystem_config.h頭檔案,可以看到其中有定義了一大推的(好像有點扯遠了啊),如下:
// [ android_filesystem_config.h ]
#define AID_ROOT 0 /* traditional unix root user */
#define AID_SYSTEM 1000 /* system server */
#define AID_RADIO 1001 /* telephony subsystem, RIL */
#define AID_BLUETOOTH 1002 /* bluetooth subsystem */
#define AID_GRAPHICS 1003 /* graphics devices */
#define AID_INPUT 1004 /* input devices */
#define AID_AUDIO 1005 /* audio devices */
#define AID_CAMERA 1006 /* camera devices */
#define AID_LOG 1007 /* log devices */
#define AID_COMPASS 1008 /* compass device */
#define AID_MOUNT 1009 /* mountd socket */
#define AID_WIFI 1010 /* wifi subsystem */
#define AID_ADB 1011 /* android debug bridge (adbd) */
2.5.2 決議permission標簽獲得系統權限和所屬的gid用戶組對應關系
前面已經解決了一個標簽,戰斗還在持續,我們繼續下一個標簽的解決,這里我們還是簡單奉上決議的xml標簽片段,如下:
<permissions>
...
<permission name="android.permission.BLUETOOTH_ADMIN" >
<group gid="net_bt_admin" />
</permission>
<permission name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
<permissions/>
我們直接看下它的決議程序,如下:
// [ SystemConfig.java ]
// 根據標簽和決議規則決議"permission"標簽
else if ("permission".equals(name) && allowPermissions) {
// 決議系統中所有權限和其所屬gid的資訊
String perm = parser.getAttributeValue(null, "name");
if (perm == null) {// 注意這里處理的就是類似android.permission.WRITE_EXTERNAL_STORAGE這種
Slog.w(TAG, "<permission> without name in " + permFile + " at "
+ parser.getPositionDescription());
XmlUtils.skipCurrentTag(parser);
continue;
}
perm = perm.intern();
// 呼叫readPermission繼續決議權限
readPermission(parser, perm);
}
上述原始碼經過一番簡單決議后,繼續呼叫readPermission方法進行下一步的處理,我們繼續跟進該方法:
void readPermission(XmlPullParser parser, String name)
throws IOException, XmlPullParserException {
// 檢查mPermissions串列,避免重復添加
if (mPermissions.containsKey(name)) {
throw new IllegalStateException("Duplicate permission definition for " + name);
}
/*
perUser表示該權限的gid是否針對設備用戶id進行調整,默認為false
并且在我們舉例中,也并沒有定義這個屬性
*/
final boolean perUser = XmlUtils.readBooleanAttribute(parser, "perUser", false);
// 創建PermissionEntry實體,并添加到mPermissions中
final PermissionEntry perm = new PermissionEntry(name, perUser);
mPermissions.put(name, perm);
int outerDepth = parser.getDepth();
int type;
while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG
|| parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG
|| type == XmlPullParser.TEXT) {
continue;
}
String tagName = parser.getName();
// 決議內部group標簽,獲得權限所屬的gid,將其添加到PermissionEntry的gids陣列中
if ("group".equals(tagName)) {
String gidStr = parser.getAttributeValue(null, "gid");
if (gidStr != null) {
// 為啥前面要搞個單獨決議group呢,直接復用這里不OK
int gid = Process.getGidForName(gidStr);
perm.gids = appendInt(perm.gids, gid);
} else {
Slog.w(TAG, "<group> without gid at "
+ parser.getPositionDescription());
}
}
XmlUtils.skipCurrentTag(parser);
}
}
可以看到在該方法中,主要決議permission標簽以及它對應的內部標簽group,得到gid和權限之間的對應關系,并將這種關系保存到SystemConfig的內部資料結構mPermissions中,并且這里我們有一點需要特別注意注意,一個權限可以對應多個gid,形如下面這個關系:
<permissions>
...
<permission name="android.permission.BLUETOOTH_STACK" >
<group gid="bluetooth" />
<group gid="wakelock" />
<group gid="uhid" />
</permission>
...
<permissions/>
2.5.3 決議assign-permission標簽獲得uid和其持有的權限關系
又攻破了一個重要的標簽,戰斗還在持續,我們繼續下一個標簽的解決,這里我們還是簡單奉上決議的xml標簽片段,如下:
<permissions>
...
<assign-permission name="android.permission.MODIFY_AUDIO_SETTINGS" uid="audioserver" />
<assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="audioserver" />
<assign-permission name="android.permission.WAKE_LOCK" uid="audioserver" />
<assign-permission name="android.permission.UPDATE_DEVICE_STATS" uid="audioserver" />
<assign-permission name="android.permission.UPDATE_APP_OPS_STATS" uid="audioserver" />
...
<permissions/>
我們直接看下它的決議程序,如下:
// [ SystemConfig.java ]
/*
如果allowPermissions為true,決議"assign-permission"標簽
決議系統中uid和其所持有的權限的關系!
*/
else if ("assign-permission".equals(name) && allowPermissions) {
String perm = parser.getAttributeValue(null, "name");//權限名稱
if (perm == null) {
Slog.w(TAG, "<assign-permission> without name in " + permFile + " at "
+ parser.getPositionDescription());
XmlUtils.skipCurrentTag(parser);
continue;
}
String uidStr = parser.getAttributeValue(null, "uid");// 擁有該權限的uid
if (uidStr == null) {
Slog.w(TAG, "<assign-permission> without uid in " + permFile + " at "
+ parser.getPositionDescription());
XmlUtils.skipCurrentTag(parser);
continue;
}
int uid = Process.getUidForName(uidStr);// 將uid轉為init值
if (uid < 0) {
Slog.w(TAG, "<assign-permission> with unknown uid \""
+ uidStr + " in " + permFile + " at "
+ parser.getPositionDescription());
XmlUtils.skipCurrentTag(parser);
continue;
}
perm = perm.intern();
ArraySet<String> perms = mSystemPermissions.get(uid);
if (perms == null) {
perms = new ArraySet<String>();
mSystemPermissions.put(uid, perms);
}
perms.add(perm);
XmlUtils.skipCurrentTag(parser);
}
對于assign-permission標簽的決議和前面的permission標簽有所差別!這里將決議assign-permission標簽,獲得uid和權限對應的映射關系,并將決議結果保存到了mSystemPermissions中,這里需要特別注意的一個點就是uid 可以對應多個權限,即它們是一對多的關系,
Android中預先指定了許多默認的uid,它們被定義在Process.java中,如下:
2.5.4 決議library標簽獲得共享庫名稱和映射關系
又攻破了一個重要的標簽,戰斗還在持續,我們繼續下一個標簽的解決,這里我們還是簡單奉上決議的xml標簽片段,如下:
<permissions>
...
<!-- 原始排版不順眼,我必須得給掰直了-->
<library name="android.test.runner" file="/system/framework/android.test.runner.jar" />
...
<permissions/>
我們直接看下它的決議程序,如下:
// [ SystemConfig.java ]
/*
如果 allowLibs為true,則決議"library"標簽
從而獲得系統中所有共享庫的資訊!
*/
else if ("library".equals(name) && allowLibs) {
String lname = parser.getAttributeValue(null, "name");
String lfile = parser.getAttributeValue(null, "file");
if (lname == null) {
Slog.w(TAG, "<library> without name in " + permFile + " at "
+ parser.getPositionDescription());
} else if (lfile == null) {
Slog.w(TAG, "<library> without file in " + permFile + " at "
+ parser.getPositionDescription());
} else {
//Log.i(TAG, "Got library " + lname + " in " + lfile);
mSharedLibraries.put(lname, lfile);
}
XmlUtils.skipCurrentTag(parser);
continue;
}
上述代碼片段通過決議library標簽,得到共享庫的名稱和路徑相關資訊并將其保存到mSharedLibraries哈希表中,其中的key表示共享庫名稱,value為共享庫路徑,
2.5.5 決議feature和unavailable-feature標簽
爆破組很累啊,攻克了一個又是一個,戰斗還在持續,我們繼續下一個標簽的解決,這里我們還是簡單奉上決議的xml標簽片段,如下:
<permissions>
...
<feature name="android.hardware.bluetooth" />
...
<permissions/>
我們直接看下它的決議程序,如下:
// [SystemConfig.java]
/*
決議feather子標簽
*/
else if ("feature".equals(name) && allowFeatures) {
String fname = parser.getAttributeValue(null, "name");
int fversion = XmlUtils.readIntAttribute(parser, "version", 0);
boolean allowed;
if (!lowRam) {
allowed = true;
} else {
// 記憶體不足時,配置了nototLowRam的feature不會在加載!
String notLowRam = parser.getAttributeValue(null, "notLowRam");
allowed = !"true".equals(notLowRam);
}
if (fname == null) {
Slog.w(TAG, "<feature> without name in " + permFile + " at "
+ parser.getPositionDescription());
} else if (allowed) {
// 將feature構造成featureInfo,加入到mAvailableFeatures物件中!
addFeature(fname, fversion);
}
XmlUtils.skipCurrentTag(parser);
continue;
}
/*
決議"unavailable-feature",如果有則將其添加到mUnavailableFeatures中
*/
else if ("unavailable-feature".equals(name) && allowFeatures) {
String fname = parser.getAttributeValue(null, "name");
if (fname == null) {
Slog.w(TAG, "<unavailable-feature> without name in " + permFile + " at "
+ parser.getPositionDescription());
} else {
mUnavailableFeatures.add(fname);
}
XmlUtils.skipCurrentTag(parser);
continue;
}
在上面的原始碼中會把組態檔中的feature和unavailable-feature標簽決議出來,可用feature會被添加到 mAvailableFeatures中,不可用feature會被添加到mUnavailableFeatures中,
這里的feature通常和硬體特性有關,譬如是否支持藍牙,nfc啊等等!
2.5.6 其它標簽的決議
其它,其它就不決議了,決議得都要吐血了!得來個腎寶,補一補!
2.6 SystemConfig決議系統組態檔小結
至此SystemConfig類以及它決議相關組態檔的流程就到這里告一段落了,此時我們可以清晰的得到SystemConfig內部的資料關系圖了,如下:

如果說Settings類是應用安裝資訊相關檔案的管理類大管家,那么這里的SystemConfig應該說就是系統相關組態檔的大管家,二者在PMKS其中扮演著非常重要的角色,實力不容小覷!
2.7 PKMS繼續處理決議完成的SystemConfig資料管家類
不知不覺,我們進入SystemConfig的分支處理中已經很久很久,久到離譜了!我們繼續回到PKMS的構造方法的BOOT_PROGRESS_PMS_START發展階段,看看對于完成決議滿血狀態的SystemConfig資料管家類進行怎么處理的:
這里我們為了排版和博客的連貫性,會跳過原始碼的順序,將后續PKMS對于SystemConfig的處理邏輯放在一起來分析!
// [ PackageManagerService.java ]
mGlobalGids = systemConfig.getGlobalGids();
mSystemPermissions = systemConfig.getSystemPermissions();
mAvailableFeatures = systemConfig.getAvailableFeatures();
這里將SystemConfig中的mGlobalGids,mSystemPermissions和mAvailableFeatures拷貝一份保存到PKMS中,我們接著繼續往下看相關處理邏輯:
//讀取權限組態檔中的資訊,保存到mPermissions這個ArrayMap中
ArrayMap<String, SystemConfig.PermissionEntry> permConfig
= systemConfig.getPermissions(); // 【2.7】
for (int i=0; i<permConfig.size(); i++) {
SystemConfig.PermissionEntry perm = permConfig.valueAt(i);
// 此時mSettings并沒有開始決議packages.xml等相關檔案,所以它里面的資料結構此時是沒有任何內容的
BasePermission bp = mSettings.mPermissions.get(perm.name);
// 加入到Settings的mPermissions中,SystemConfig決議的權限的包名都為android
if (bp == null) {
// BasePermission.TYPE_BUILTIN表示的是在編譯時期就確定好的,系統要提供的權限!
bp = new BasePermission(perm.name, "android", BasePermission.TYPE_BUILTIN); //詳見 【2.7.2】
mSettings.mPermissions.put(perm.name, bp);
}
if (perm.gids != null) {
/*
如果系統權限有所屬的gids,將其添加到BasePermission物件中
這個邏輯比較簡單就不展開了
*、
bp.setGids(perm.gids, perm.perUser);
}
}
//獲取并處理所有共享庫資訊
ArrayMap<String, String> libConfig = systemConfig.getSharedLibraries();
for (int i=0; i<libConfig.size(); i++) {
mSharedLibraries.put(libConfig.keyAt(i),
new SharedLibraryEntry(libConfig.valueAt(i), null));
}
這里可以看到PKMS繼續處理權限資訊和共享庫資訊的邏輯,將從SystemConfig中決議出來的mPermissions資訊保存到Setting的mPermissions中,并且將權限所屬的包名指定為android,這也是packages.xml中<permissions>標簽資料的由來,
在此處有幾個關鍵點我們需要理解:
1. 此時mSettings并沒有開始決議packages.xml等相關檔案,所以它里面的資料結構mPermissions此時是沒有任何內容的
2. 此時SystemConfig.mPermissions中保存的是系統中定義的權限和對應的gid之間的映射關系
2.7.1 PKMS授權機制簡介
在開始BasePermission的介紹前,非常必須且有必要先來簡單介紹下PKMS的授權機制的設計,它設計的最最主要用途是用來判定是否授權以及授權型別,
Android中,有權限持有者和權限申請者兩個角色,一個Android包可以扮演其中一種角色,或兩種角色兼備,持有者通過設計權限來保護介面和資料,申請者如果要訪問受保護的介面和資料時,需要事先宣告,然后交由包管理者來判斷是否要授權,
對于權限持有者而言,可以通過protectionLevel屬性定義了權限的受保護級別,其取值主要有以下四種:
- normal(0): 最普通的一類權限,只要申請使用這一類權限就授予,
- dangerous(1): 較為危險的一類權限,譬如訪問聯系人、獲取位置服務等權限,需要經過用戶允許才授予,
- signature(2): 如果申請者與該權限的持有者簽名相同,則授予這類權限,
- signatureOrPrivileged(18): 對singature的一個補充,權限申請者與持有者簽名相同,或者申請者是位于/system/priv-app目錄下的應用,則授予這類權限,在早期的Android版本,所有系統應用都位于/system/app目錄下,其定義為signatureOrSystem(3),但該定義已經過時了;當Android引入了/system/priv-app目錄以后,就將這一類保護級別重新定義為signatureOrPrivileged(18),
并且對于權限持有者而言,會在它對外提供的API介面中,通過呼叫類似checkPermission方法檢測呼叫者是否擁有權限,如果沒有則會提示對應資訊或者拋出例外!
有了上面的權限級別限制,就可以理解,部分權限安裝時就被授予,而部分權限需要申請者滿足一定的條件才能被授予,從Android M(6.0)開始,對最終授予的權限進行了分類:
- install:安裝時授予的權限,normal、signature、signatureOrPrivilege的授予都屬于這一類,
- runtime:運行時由用戶決定是否授予的權限,在Android M(6.0)以前,dangerous的權限屬于install型別,但從Android M(6.0)以后,dangerous的權限改為屬于runtime一類了,在使用這類權限時,會彈出一個對話框,讓用戶選擇是否授權,
注意,授權型別是Android M(6.0)才引進的,之前只有是否授權的區分,之所以做授權型別的區分,是為了適應多用戶使用的場景,install型別的授權,對所有用戶都是一樣的;runtime型別的授權,不同用戶使用的選擇不一樣,譬如一個用戶在使用的會授予某個應用訪問聯系人資料的權限,但另一個用戶使用時會選擇拒絕授權,
2.7.2 BasePermission權限持有者封裝類
為了對權限持有者申明的權限進行管理特意構建了BasePermission資料結構對其進行管理,好了,有了以上的初步了解我們直接從原始碼層面來分析一下BasePermission,上原始碼!
// [ BasePermission.java ]
private int[] gids;// 擁有該權限的gid組
BasePermission(String _name, String _sourcePackage, int _type) {
name = _name;// 權限名
sourcePackage = _sourcePackage;// 定義權限的包名,即權限的持有者
type = _type;// 權限型別
protectionLevel = PermissionInfo.PROTECTION_SIGNATURE;//權限的級別,這個前面有提到過了
}
并且Android系統將權限分為如下有三種型別進行管理:
final static int TYPE_NORMAL = 0;//通用型別
final static int TYPE_BUILTIN = 1;//構建型別(這種型別的權限,通常必須只能系統層才能被定義)
final static int TYPE_DYNAMIC = 2;//動態型別
2.8 PKMS處理SystemConfig資料管家小結
至此PKMS對于SystemConfig大管家的讀取操作到這里就基本結束了,通過前面的分析我們可以得出如下的結論就是SystemConfig保存的均是和系統相關的屬性資訊,比如系統權限,系統特性,gid等等,而通過后續的分析我們會知道Settings應用安裝資訊管家不僅會保存系統的也會保存安裝應用所有的資訊,所以這里需要將SystemConfig中的權限資訊拷貝一份到Settings中,
在對本篇博客余下邏輯分析前,我們必須且必要對于SystemConfig、Settings以及PKMS之間牽涉的資料關系之間的映射進行一下階段性的總結,如下:

這里最最重要的一點就是通過SystemConfig的決議,我們可以得到一些系統定義的一些配置資訊:
- 系統定義的一些gid;
- 系統定義的權限和uid的映射關系;
- 系統定義的權限和gid的映射關系;
- 系統共享庫和feature;
這里給讀者拋出一個問題?就是為什么PKMS中要重復設計一套和SystemConfig中相同的資料結構來將它備份一遍呢,而不是直接使用SystemConfig中的呢,這樣不是浪費記憶體空間嗎(雖然這點資料空間微不足道)!讀者朋友可以帶這個這個疑問繼續接下來的相關分析,我想你會找到滿意的答案的,
2.9 構建PackageHandler實體以及系結訊息處理執行緒
在該邏輯中我們會創建PackageHandler物件,將其系結到一個ServiceThread后臺執行緒的訊息佇列中,并加入到Watchdog的監控串列中,
這里為啥PackageHandler系結的不是PKMS的主執行緒呢?這是為什么呢?
如果讀者對應用的安裝整個流程有一定了解的話,應該知道應用的安裝和卸載是一個比較耗時的操作,所以這些厚重的耗時操作必須開辟專門的執行緒來處理,就交由這個后臺執行緒完成了,并且由于PKMS是一個重要的系統服務,這個后臺執行緒的訊息佇列如果過于忙碌,則會導致系統一直卡住,所以需要將這個訊息佇列加入Watchdog的監控串列,以便在這種情況下,Watchdog可以做出一些應急操作,譬如重啟PKMS所屬的system_serverin行程等相關操作,
這里我們對該邏輯簡單分析一下:
// [ PackageManagerService.java ]
/*
建立并啟動一個名為 “PackageManager” 的 ServiceThread,用于處理一些耗時的操作!
*/
mHandlerThread = new ServiceThread(TAG,
Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
mHandlerThread.start();
/*
創建一個 PackageHandler物件,系結前面創建的 HandlerThread!
*/
mHandler = new PackageHandler(mHandlerThread.getLooper());// 【 2.9.1 ]
這里的ServiceThread繼承HandlerThread,專門為系統服務定義的(從它的名稱就可以看出該Thread是用來和Handler進行系結的)!
2.9.1 PackageHandler的訊息處理
PackageHandler歸根究底是一個Handler子類,所以一定逃不出真香定律,通常我們對于Hhandler重點要關注的是它的handleMessage方法,我們簡單來看看它處理了那些訊息,
// [ PackageManagerService.java ]
class PackageHandler extends Handler {
...
public void handleMessage(Message msg) {
try {
doHandleMessage(msg);
} finally {
...
}
}
void doHandleMessage(Message msg) {
switch (msg.what) {
case INIT_COPY: {// 安裝拷貝階段
...
}
case MCS_BOUND: { //系結階段
...
}
case MCS_RECONNECT: {
...
}
case MCS_UNBIND: {
...
}
case SEND_PENDING_BROADCAST: {
...
}
case START_CLEANING_PACKAGE: {
...
}
...
}
}
}
這里我們可以看到關于應用安裝,卸載處理的一些耗時的操作,PKMS都會放在Handler中進行相關的處理,這個邏輯在后續分析第三方應用的安裝和卸載中會很明顯的看到,
2.10 構建UserManagerService用戶管理服務
尼瑪,我要被整吐了PKMS啟動BOOT_PROGRESS_PMS_START發展階段的內容好多啊,尼瑪太累了,分析原始碼嗎,感覺囫圇吞棗也不行,匆匆帶過也不行,難啊!此階段還剩最好一個邏輯就是構建UserManagerService用戶管理服務,我們簡單來看下邏輯:
// [ UserManagerService.java ]
private UserManagerService(Context context, PackageManagerService pm,
Object packagesLock, File dataDir) {
mContext = context;
mPm = pm;
mPackagesLock = packagesLock;
mHandler = new MainHandler();
synchronized (mPackagesLock) {
// mUsersDir=/data/system/users
mUsersDir = new File(dataDir, USER_INFO_DIR);
mUsersDir.mkdirs();
// userZeroDir=/data/system/users/0
File userZeroDir = new File(mUsersDir, String.valueOf(UserHandle.USER_SYSTEM));
userZeroDir.mkdirs();
FileUtils.setPermissions(mUsersDir.toString(),
FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IROTH | FileUtils.S_IXOTH,
-1, -1);
// mUserListFile=/data/system/users/userlist.xml
mUserListFile = new File(mUsersDir, USER_LIST_FILENAME);
initDefaultGuestRestrictions();
readUserListLP();
sInstance = this;
}
mLocalService = new LocalService();
LocalServices.addService(UserManagerInternal.class, mLocalService);
mLockPatternUtils = new LockPatternUtils(mContext);
mUserStates.put(UserHandle.USER_SYSTEM, UserState.STATE_BOOTING);
}
它的構造構造程序比較簡單,就是創建幾個目錄和幾個檔案:
-
/data/system/users
-
/data/system/users/0
-
/data/system/users/userlist.xml
接著通過呼叫readUserList方法讀取用戶串列,這里就不對該方法展開了,該方法就是從userlist.xml檔案中讀取用戶資訊,保存到UserManager的成員變數mUsers中,
過年了,快放假了本篇的博客肯定是寫不完了,這里現將放出來,讓感興趣的讀者可以先行閱讀,不感興趣的估計也不會看到這里了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/257820.html
標籤:其他
上一篇:MarkDown學習
下一篇:均分糖果——2021年2-7更


