---
title: 'PackageManagerService - APK 解析'
disqus: kyleAlien
---
PackageManagerService - APK 解析
===
:::success
PackageManagerService 主流程請看 [**PackageManagerService 篇**](https://hackmd.io/zlyaXIOJR-mUdvdk3u84Gg?view)
:::
## OverView of Content
[TOC]
## APK 解析
在 PKMS 的建構函數中 (第二、三階段) 有使用到 **scanDirTracedLI** 方法來掃描某個目錄的 APK 文件
以下是 Android 10.0 的 **系統 APP**
| APP 類別 | 目錄 | other |
| -------- | -------- | -------- |
| 系統 APP | /vendor/overlay | |
| 系統 APP | /product/overlay | |
| 系統 APP | /product_services/overlay | |
| 系統 APP | /odm/overlay | |
| 系統 APP | /oem/overlay | |
| 系統 APP | /system/framework |  |
| 系統 APP | /system/priv-app |  |
| 系統 APP | /system/app |  |
| 系統 APP | /vendor/priv-app | |
| 系統 APP | /vendor/app | |
| 系統 APP | /odm/priv-app | |
| 系統 APP | /odm/app | |
| 系統 APP | /oem/app | |
| 系統 APP | /oem/priv-app | |
| 系統 APP | /product/priv-app | |
| 系統 APP | /product/app | |
| 系統 APP | /product_services/priv-app | |
| 系統 APP | /product_services/app | |
### scanDirTracedLI 掃描資料夾
* PKMS#**scanDirTracedLI** 方法取名:是指在 `Lock mInstallLock` 的狀態下掃描 目錄 & APK 文件
:::warning
* PKMS#方法取名規則:xxx(方法名) + LI、LIF、LPw、LPr ? [**參考**](https://david1840.github.io/2021/05/13/Android%E7%B3%BB%E7%BB%9F-PMS-scanPackageDirtyLI%E7%9A%84LI%E6%98%AF%E4%BB%80%E4%B9%88%E6%84%8F%E6%80%9D/)
| 關鍵字 | 說明 |
| -------- | -------- |
| L | Lock |
| I | mInstallLock |
| P | mPackages |
| w | writing |
| r | reading |
| F | Freeze |
scanDirTracedLI 就是在呼叫該方法前需要保證鎖住 mInstallLock 對象
:::
* PKMS#scanDirTracedLI 方法會根據傳入的第一個參數決定要掃描哪一個目錄
```java=
// PackageManagerService.java
private void scanDirTracedLI(File scanDir, final int parseFlags, int scanFlags,
long currentTime, PackageParser2 packageParser, ExecutorService executorService) {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanDir [" + scanDir.getAbsolutePath() + "]");
try {
// @ 分析 scanDirLI
scanDirLI(scanDir, parseFlags, scanFlags, currentTime,
packageParser, // Parser APK 類
executorService);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}
```
### scanDirLI - [ParallelPackageParser](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/pm/ParallelPackageParser.java) 解析隊列
* PKMS#scanDirLI 會使用 ParallelPackageParser (對列類) 來收集目標資料夾內的 APK 文件並解析(解析部份後面分析)
```java=
// PackageManagerService.java
private void scanDirLI(File scanDir, int parseFlags, int scanFlags, long currentTime,
PackageParser2 packageParser, ExecutorService executorService) {
// 指定目錄下的所有檔案
final File[] files = scanDir.listFiles();
... 省略判空
ParallelPackageParser parallelPackageParser =
new ParallelPackageParser(packageParser, executorService);
// Submit files for parsing in parallel
int fileCount = 0;
for (File file : files) {
// 是 APK or 目錄
final boolean isPackage = (isApkFile(file) || file.isDirectory())
&& !PackageInstallerService.isStageName(file.getName());
if (!isPackage) {
// 過濾非 APK 文件
continue;
}
// 把 APK 文件存入 ParallelPackageParser
// @ 分析 ++submit 函數++
parallelPackageParser.submit(file, parseFlags);
fileCount++;
}
// 一個個處理 submit 掃描的結果
for (; fileCount > 0; fileCount--) {
// 取出掃描結過
ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take();
Throwable throwable = parseResult.throwable;
int errorCode = PackageManager.INSTALL_SUCCEEDED;
String errorMsg = null;
if (throwable == null) {
// 使用靜態 library
if (parseResult.parsedPackage.isStaticSharedLibrary()) {
renameStaticSharedLibraryPackage(parseResult.parsedPackage);
}
try {
// 在手機平台初始化時添加新的 package 到 內部 data 中
addForInitLI(parseResult.parsedPackage,
parseFlags,
scanFlags,
currentTime,
null);
} /* 省略 catch */
} /* 省略其他類型的錯誤 */
... 省略部分
// 如果非系統 APK && 解析失敗
if ((scanFlags & SCAN_AS_SYSTEM) == 0
&& errorCode != PackageManager.INSTALL_SUCCEEDED) {
logCriticalInfo(Log.WARN,
"Deleting invalid package at " + parseResult.scanFile);
// 刪除 APP 文件
removeCodePathLI(parseResult.scanFile);
}
}
}
```
* [**ParallelPackageParser**](https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/pm/ParallelPackageParser.java)#submit:把路徑中的 APK 內容異步線程池中,並在解析完後把 ParseResult 結果放入 BlockingQueue 隊列
```java=
// ParallelPackageParser.java
// QUEUE_CAPACITY = 30
private final BlockingQueue<ParseResult> mQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
private final PackageParser2 mPackageParser;
private final ExecutorService mExecutorService;
ParallelPackageParser(PackageParser2 packageParser, ExecutorService executorService) {
mPackageParser = packageParser;
mExecutorService = executorService;
}
public void submit(File scanFile, int parseFlags) {
// 使用線程池
mExecutorService.submit(() -> {
ParseResult pr = new ParseResult();
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parallel parsePackage [" + scanFile + "]");
try {
pr.scanFile = scanFile;
// @ 注意 ++parsePackage++ 函數
pr.parsedPackage = parsePackage(scanFile, parseFlags);
} catch (Throwable e) {
pr.throwable = e;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
try {
// 將處理好的任務放入對列
mQueue.put(pr);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// Propagate result to callers of take().
// This is helpful to prevent main thread from getting stuck waiting on
// ParallelPackageParser to finish in case of interruption
mInterruptedInThread = Thread.currentThread().getName();
}
});
}
@VisibleForTesting
protected ParsedPackage parsePackage(File scanFile, int parseFlags)
throws PackageParser.PackageParserException {
// @ 調用 PackageParser2#mPackageParser
return mPackageParser.parsePackage(scanFile, parseFlags, true);
}
```
> 
### PackageParser2 - 解析 APK 文件
* ParallelPackageParser 類的 submit 方法最終會調用 [**PackageParser2**](https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/pm/parsing/PackageParser2.java)#parsePackage: 對 APK 進行解析 (調用 ParsingPackageUtils 解析)
```java=
// PackageParser2.java
private ParsingPackageUtils parsingUtils;
private ThreadLocal<ParseTypeImpl> mSharedResult;
public ParsedPackage parsePackage(File packageFile, int flags, boolean useCaches)
throws PackageParserException {
... 省略部分
ParseInput input = mSharedResult.get().reset();
// 透過 ParsingPackageUtils 解析 APK
ParseResult<ParsingPackage> result = parsingUtils.parsePackage(input, packageFile, flags);
if (result.isError()) {
throw new PackageParserException(result.getErrorCode(), result.getErrorMessage(),
result.getException());
}
ParsedPackage parsed = (ParsedPackage) result.getResult().hideAsParsed();
... 省略部分
return parsed;
}
```
* [**ParsingPackageUtils**](https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/content/pm/parsing/ParsingPackageUtils.java)#parsePackage: ^1^ 如果傳入是目錄調用 `parseClusterPackage`,^2^ 否則調用 `parseMonolithicPackage` 解析 APK
:::info
* **Split APK 機制**
在 Android 5.0 後引入了 Split APK 機制,其目的是為了解決 DVM 對方法的 65535 上限限制,透過 Split 機制可以將大型 APK 切成多個獨立 APK
1. Single APK:一個完整個 APK 文件,由單個 baseAPK 完成,**又稱為 Monlithic**
2. Mutiple APK:在一個文件目錄中安裝應用,它是由一個 `baseAPK` + 多個 `splitApk` 組成,**又稱為 Cluster**
> 
:::
以下分析較難的 parseClusterPackage 方法
```java=
// ParsingPackageUtils.java
public ParseResult<ParsingPackage> parsePackage(ParseInput input, File packageFile,
int flags)
throws PackageParserException {
if (packageFile.isDirectory()) {
// 多個文件
// @ 分析 parseClusterPackage
return parseClusterPackage(input, packageFile, flags);
} else {
// 解析 單個 APK 文件
return parseMonolithicPackage(input, packageFile, flags);
}
}
```
> 
* parseClusterPackage 分析
1. 輕量解析目錄文件
2. 判斷是否是核心應用,如果是該 APP 不是核心應用,但目前是 CoreApps 模式(詳細需要往上查找 PKMS 創建),則會拋出異常
3. 解析 baseAPK 文件,下面繼續分析 parseBaseApk 方法
4. 解析 splitAPK 文件(1 ~ 多個)
```java=
// ParsingPackageUtils.java
private ParseResult<ParsingPackage> parseClusterPackage(ParseInput input, File packageDir,
int flags) {
// 1. 輕量解析目錄文件
final ParseResult<PackageLite> liteResult =
ApkLiteParseUtils.parseClusterPackageLite(input, packageDir, 0);
if (liteResult.isError()) {
return input.error(liteResult);
}
final PackageLite lite = liteResult.getResult();
// 2. 是否只解析核心應用
// 所謂的核心是為了創建極簡的啟動環境
//
// mOnlyCoreApps:只要設備加密就會是 true (從外部傳入)
// lite.isCoreApp():當前包是否含有核心應用,對應 AndroidManifest 中的 coreApp 值
if (mOnlyCoreApps && !lite.isCoreApp()) {
return input.error(INSTALL_PARSE_FAILED_ONLY_COREAPP_ALLOWED,
"Not a coreApp: " + packageDir);
}
... 省略部份
try {
// 3. 取得 baseAPK 檔案
final File baseApk = new File(lite.getBaseApkPath());
// 解析複製好的 baseAPK 檔案
// 分析 @ parseBaseApk
final ParseResult<ParsingPackage> result = parseBaseApk(input, baseApk,
lite.getPath(), assetLoader, flags);
if (result.isError()) {
return input.error(result);
}
ParsingPackage pkg = result.getResult();
// 判斷是否有 splitApk
if (!ArrayUtils.isEmpty(lite.getSplitNames())) {
pkg.asSplit(
lite.getSplitNames(),
lite.getSplitApkPaths(),
lite.getSplitRevisionCodes(),
splitDependencies
);
// 取得 splitApk 的數量
final int num = lite.getSplitNames().length;
for (int i = 0; i < num; i++) {
final AssetManager splitAssets = assetLoader.getSplitAssetManager(i);
// 解析每個 splitApk 檔案
parseSplitApk(input, pkg, i, splitAssets, flags);
}
}
pkg.setUse32BitAbi(lite.isUse32bitAbi());
return input.success(pkg);
} catch (PackageParserException e) {
return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"Failed to load assets: " + lite.getBaseApkPath(), e);
} finally {
IoUtils.closeQuietly(assetLoader);
}
}
```
> 
### [ParsingPackageUtils](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/pm/parsing/ParsingPackageUtils.java) - parseBaseApk 分析 AndroidManifest.xml
* 當找到要分析的目標 APK 後,就會調用 ParsingPackageUtils#parseBaseApk 方法
1. 如果 baseApk 絕對路徑是以 `/mnt/expand/` 開頭,就取 `/mnt/expand/` 後的設定為 volumeUuid
2. 透過 AssetManager 解析 baseApk 中的 `AndroidManifest.xml` 文件
3. 呼叫重載方法 parseBaseApk
* 讀取 [**attrs_manifest.xml**](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/attrs_manifest.xml) 內的屬性集
* 透過 parseBaseApkTags 方法分析 AndroidManifest 基本屬性
```java=
// ParsingPackageUtils.java
public static final String MNT_EXPAND = "/mnt/expand/";
public static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";
private ParseResult<ParsingPackage> parseBaseApk(ParseInput input, File apkFile,
String codePath, SplitAssetLoader assetLoader, int flags)
throws PackageParserException {
// 取得 baseApk 絕對位置
final String apkPath = apkFile.getAbsolutePath();
// 1. 取得 volumeUuid
String volumeUuid = null;
if (apkPath.startsWith(MNT_EXPAND)) {
final int end = apkPath.indexOf('/', MNT_EXPAND.length());
volumeUuid = apkPath.substring(MNT_EXPAND.length(), end);
}
... log 訊息
// 取得 AssetManager
final AssetManager assets = assetLoader.getBaseAssetManager();
final int cookie = assets.findCookieForPath(apkPath);
if (cookie == 0) {
return input.error(INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Failed adding asset path: " + apkPath);
}
// 2. 指定分析 AndroidManifest.xml 文件
try (XmlResourceParser parser = assets.openXmlResourceParser(cookie,
ANDROID_MANIFEST_FILENAME)) {
final Resources res = new Resources(assets, mDisplayMetrics, null);
// 3. @ 分析 parseBaseApk
ParseResult<ParsingPackage> result = parseBaseApk(input, apkPath, codePath, res,
parser, flags);
... 省略部分
// 用於以後標示這個解析後的 Package
pkg.setVolumeUuid(volumeUuid);
...
return input.success(pkg);
} /* 省略 catch */
}
// ParseResult 方法重載
private ParseResult<ParsingPackage> parseBaseApk(ParseInput input, String apkPath,
String codePath, Resources res, XmlResourceParser parser, int flags)
throws XmlPullParserException, IOException {
... 省略部分
// 屬性集在 attrs_manifest.xml
final TypedArray manifestArray = res.obtainAttributes(parser, R.styleable.AndroidManifest);
try {
final boolean isCoreApp =
parser.getAttributeBooleanValue(null, "coreApp", false);
final ParsingPackage pkg = mCallback.startParsingPackage(
pkgName, apkPath, codePath, manifestArray, isCoreApp);
// @ 分析 parseBaseApkTags
final ParseResult<ParsingPackage> result =
parseBaseApkTags(input, pkg, manifestArray, res, parser, flags);
if (result.isError()) {
return result;
}
return input.success(pkg);
} finally {
// TypedArray 必須回收
manifestArray.recycle();
}
}
```
> 
* ParsingPackageUtils#`parseBaseApkTags` 分析 Apk 中的 AndroidManifest 的 Application、Activity、Service、Broadcast、ContentProvider 等等訊息
```java=
// ParsingPackageUtils.java
private ParseResult<ParsingPackage> parseBaseApkTags(ParseInput input, ParsingPackage pkg,
TypedArray sa, Resources res, XmlResourceParser parser, int flags)
throws XmlPullParserException, IOException {
... 省略部分
final int depth = parser.getDepth();
int type;
// 在這裡會 Parser 完 AndroidManifest 全部的內容
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG
|| parser.getDepth() > depth)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
String tagName = parser.getName();
final ParseResult result;
if (TAG_APPLICATION.equals(tagName)) {
if (foundApp) {
// 一個 APP 只能有一個 Application 標籤
if (RIGID_PARSER) {
result = input.error("<manifest> has more than one <application>");
} else {
Slog.w(TAG, "<manifest> has more than one <application>");
result = input.success(null);
}
} else {
foundApp = true;
// Parser 四大組件
// Activity、Service、Broadcast、ContentProvider
result = parseBaseApplication(input, pkg, res, parser, flags);
}
} else {
// Parser 基本的元素,像權限組之類的
// @ parseBaseApkTag
result = parseBaseApkTag(tagName, input, pkg, res, parser, flags);
}
... 省略部分
}
... 省略部分
return input.success(pkg);
}
private ParseResult parseBaseApkTag(String tag, ParseInput input,
ParsingPackage pkg, Resources res, XmlResourceParser parser, int flags)
throws IOException, XmlPullParserException {
switch (tag)
case TAG_PERMISSION:
return parsePermission(input, pkg, res, parser);
... 省略其他 case
}
}
```
> 
### [ParsingPackageImpl](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/pm/parsing/ParsingPackageImpl.java) - 儲存 Package 訊息
* Android 組件解析關係圖,解析的實作交給 Parsed 實作類 (`ParsedActivity`、`ParsedService`... 等等)
> 
* ParsingPackageImpl 類負責儲存 APK 最終分析的結果,其中就包含了 `4 大組件`、 `packageName`、`versionCode`... 等等訊息
1. 每個組件中都含有 xxxInfo 數據、該 Info 才是該組件的數據
> * Activity 內會有 [**ActivityInfo**](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/pm/ActivityInfo.java)
>
> * Service 內會有 [**ServiceInfo**](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/pm/ServiceInfo.java)
2. 四大組件的標籤內有包含 `<intent-filter\>`,用來過濾 Intent 訊息,其 **Package 結果也會保存在 ParsingPackageImpl 中** (透過 ParsedIntentInfoUtils 類解析)
:::info
其實就是保存了 **AndroidManifest.xml** 的訊息
:::
```java=
// ParsingPackageImpl.java
public class ParsingPackageImpl implements ParsingPackage, Parcelable {
private static final String TAG = "PackageImpl";
...
// Package 基本資料
protected int versionCode;
protected int versionCodeMajor;
private int baseRevisionCode;
@Nullable
@DataClass.ParcelWith(ForInternedString.class)
private String versionName;
private int compileSdkVersion;
@Nullable
@DataClass.ParcelWith(ForInternedString.class)
private String compileSdkVersionCodeName;
@NonNull
@DataClass.ParcelWith(ForInternedString.class)
protected String packageName;
@Nullable
@DataClass.ParcelWith(ForInternedString.class)
private String realPackage;
@NonNull
protected String mBaseApkPath;
// 四大組件 + 權限
@NonNull
protected List<ParsedActivity> activities = emptyList();
@NonNull
protected List<ParsedActivity> receivers = emptyList();
@NonNull
protected List<ParsedService> services = emptyList();
@NonNull
protected List<ParsedProvider> providers = emptyList();
@NonNull
private List<ParsedAttribution> attributions = emptyList();
@NonNull
protected List<ParsedPermission> permissions = emptyList();
@NonNull
protected List<ParsedPermissionGroup> permissionGroups = emptyList();
@NonNull
protected List<ParsedInstrumentation> instrumentations = emptyList();
// Intent info 訊息
@NonNull
@DataClass.ParcelWith(ParsedIntentInfo.ListParceler.class)
private List<Pair<String, ParsedIntentInfo>> preferredActivityFilters = emptyList();
@NonNull
private Map<String, ParsedProcess> processes = emptyMap();
... 省略部份
}
```
### Package 解析結果 - 存到 PKMS Map 中
先回顧 scanDirLI 方法掃描完 APK 後,緊接著就是執行 `addForInitLI` 方法,將掃描結果除存到 PKMS 中,方便之後取用
```java=
// PackageManagerService.java
private void scanDirLI(File scanDir, int parseFlags, int scanFlags, long currentTime,
PackageParser2 packageParser, ExecutorService executorService) {
// 指定目錄下的所有檔案
final File[] files = scanDir.listFiles();
... 省略判空
ParallelPackageParser parallelPackageParser =
new ParallelPackageParser(packageParser, executorService);
int fileCount = 0;
for (File file : files) {
... 掃描 apk 文件
}
// 一個個處理 submit 掃描的結果
for (; fileCount > 0; fileCount--) {
// 取出掃描結過
ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take();
Throwable throwable = parseResult.throwable;
int errorCode = PackageManager.INSTALL_SUCCEEDED;
String errorMsg = null;
if (throwable == null) {
...
try {
// @ 追蹤 addForInitLI 方法
addForInitLI(parseResult.parsedPackage,
parseFlags,
scanFlags,
currentTime,
null);
} /* 省略 catch */
} /* 省略其他類型的錯誤 */
... 省略部分
}
}
```
* PKMS#addForInitLI 方法:會在 PKMS 中儲存一個 Package 結構包,方便系統經後查詢(對於會對於 `/system`、`/vendor` 中的 APK 進行版本跟簽名檢查)
:::danger
如果不能通過簽名檢查,則 APP 資料會從 `/data` 中被移除
:::
```java=
// PackageManagerService.java
private AndroidPackage addForInitLI(ParsedPackage parsedPackage,
@ParseFlags int parseFlags, @ScanFlags int scanFlags, long currentTime,
@Nullable UserHandle user)
throws PackageManagerException {
... 省略檢查
final ScanResult scanResult = scanPackageNewLI(parsedPackage, parseFlags, scanFlags
| SCAN_UPDATE_SIGNATURE, currentTime, user, null);
if (scanResult.success) {
synchronized (mLock) {
boolean appIdCreated = false;
try {
final String pkgName = scanResult.pkgSetting.name;
final Map<String, ReconciledPackage> reconcileResult = reconcilePackagesLocked(
new ReconcileRequest(
Collections.singletonMap(pkgName, scanResult),
mSharedLibraries,
mPackages,
Collections.singletonMap(
pkgName, getSettingsVersionForPackage(parsedPackage)),
Collections.singletonMap(pkgName,
getSharedLibLatestVersionSetting(scanResult))),
mSettings.getKeySetManagerService(), mInjector);
appIdCreated = optimisticallyRegisterAppId(scanResult);
// @ 追蹤 commitReconciledScanResultLocked 方法
commitReconciledScanResultLocked(
reconcileResult.get(pkgName), mUserManager.getUserIds());
} /* 省略 catch */
}
}
... 省略部份
return scanResult.pkgSetting.pkg;
}
```
* PKMS#commitReconciledScanResultLocked 方法:提交掃描好的 Package 資訊,並修改系統狀態
```java=
// PackageManagerService.java
private AndroidPackage commitReconciledScanResultLocked(
@NonNull ReconciledPackage reconciledPkg, int[] allUsers) {
final ScanResult result = reconciledPkg.scanResult;
final ScanRequest request = result.request;
... 省略部份
final int userId = user == null ? 0 : user.getIdentifier();
// 修正 package 狀態
// @ 追蹤 commitPackageSettings 方法
commitPackageSettings(pkg, oldPkg, pkgSetting, oldPkgSetting, scanFlags,
(parseFlags & ParsingPackageUtils.PARSE_CHATTY) != 0 /*chatty*/, reconciledPkg);
if (pkgSetting.getInstantApp(userId)) {
mInstantAppRegistry.addInstantAppLPw(userId, pkgSetting.appId);
}
pkgSetting.setStatesOnCommit();
return pkg;
}
```
* PKMS 會將掃描好的 apk 檔案儲存到 `WatchedArrayMap` 中,將來要透過 PKMS 來找尋對應 APK 資料時就會到 `WatchedArrayMap` 中尋找
| Key | Value |
| -------- | -------- |
| Package name | AndroidPackage |
```java=
// PackageManagerService.java
final WatchedArrayMap<String, AndroidPackage> mPackages = new WatchedArrayMap<>();
private void commitPackageSettings(@NonNull AndroidPackage pkg,
/* 省略部份參數 */) {
// 取得分析好的 Package name
final String pkgName = pkg.getPackageName();
... 省略部份
synchronized (mLock) {
...
// 將 package 訊息添加到 mPackages 表中
mPackages.put(pkg.getPackageName(), pkg);
...
}
}
```
## Appendix & FAQ
:::info
:::
###### tags: `Android Framework`