Android包管理機(jī)制(一) PackageInstaller的初始化
前言
包管理機(jī)制是Android中的重要機(jī)制,是應(yīng)用開(kāi)發(fā)和系統(tǒng)開(kāi)發(fā)需要掌握的知識(shí)點(diǎn)之一。
包指的是Apk、jar和so文件等等,它們被加載到Android內(nèi)存中,由一個(gè)包轉(zhuǎn)變成可執(zhí)行的代碼,這就需要一個(gè)機(jī)制來(lái)進(jìn)行包的加載、解析、管理等操作,這就是包管理機(jī)制。包管理機(jī)制由許多類(lèi)一起組成,其中核心為PackageManagerService(PMS),它負(fù)責(zé)對(duì)包進(jìn)行管理,如果直接講PMS會(huì)比較難以理解,因此我們需要一個(gè)切入點(diǎn),這個(gè)切入點(diǎn)就是常見(jiàn)的APK的安裝。
講到APK的安裝之前,先了解下PackageManager、APK文件結(jié)構(gòu)和安裝方式。
1.PackageManager簡(jiǎn)介
與ActivityManager和AMS的關(guān)系類(lèi)似,PMS也有一個(gè)對(duì)應(yīng)的管理類(lèi)PackageManager,用于向應(yīng)用程序進(jìn)程提供一些功能。PackageManager是一個(gè)抽象類(lèi),它的具體實(shí)現(xiàn)類(lèi)為ApplicationPackageManager,ApplicationPackageManager中的方法會(huì)通過(guò)IPackageManager與AMS進(jìn)行進(jìn)程間通信,因此PackageManager所提供的功能最終是由PMS來(lái)實(shí)現(xiàn)的,這么設(shè)計(jì)的主要用意是為了避免系統(tǒng)服務(wù)PMS直接被訪問(wèn)。PackageManager提供了一些功能,主要有以下幾點(diǎn):
- 獲取一個(gè)應(yīng)用程序的所有信息(ApplicationInfo)。
- 獲取四大組件的信息。
- 查詢(xún)permission相關(guān)信息。
- 獲取包的信息。
- 安裝、卸載APK.
2.APK文件結(jié)構(gòu)和安裝方式
APK是AndroidPackage的縮寫(xiě),即Android安裝包,它實(shí)際上是zip格式的壓縮文件,一般情況下,解壓后的文件結(jié)構(gòu)如下表所示。
| 目錄/文件 | 描述 |
|---|---|
| assert | 存放的原生資源文件,通過(guò)AssetManager類(lèi)訪問(wèn)。 |
| lib | 存放庫(kù)文件。 |
| META-INF | 保存應(yīng)用的簽名信息,簽名信息可以驗(yàn)證APK文件的完整性。 |
| res | 存放資源文件。res中除了raw子目錄,其他的子目錄都參與編譯,這些子目錄下的資源是通過(guò)編譯出的R類(lèi)在代碼中訪問(wèn)。 |
| AndroidManifest.xml | 用來(lái)聲明應(yīng)用程序的包名稱(chēng)、版本、組件和權(quán)限等數(shù)據(jù)。 apk中的AndroidManifest.xml經(jīng)過(guò)壓縮,可以通過(guò)AXMLPrinter2工具解開(kāi)。 |
| classes.dex | Java源碼編譯后生成的Java字節(jié)碼文件。 |
| resources.arsc | 編譯后的二進(jìn)制資源文件。 |
APK的安裝場(chǎng)景主要有以下幾種:
- 通過(guò)adb命令安裝:adb 命令包括adb push/install
- 通過(guò)系統(tǒng)安裝器packageinstaller進(jìn)行安裝:packageinstaller是系統(tǒng)內(nèi)置的應(yīng)用程序,用于安裝和卸載應(yīng)用程序。
- 系統(tǒng)應(yīng)用安裝
- 應(yīng)用商店自動(dòng)安裝
這4種方式最終都會(huì)調(diào)用PMS的scanPackageDirtyLI方法用來(lái)解析包,在此之前的調(diào)用鏈?zhǔn)遣煌模酒恼聲?huì)介紹第二種方式,對(duì)于用戶(hù)來(lái)說(shuō),這是最常用的安裝方式;對(duì)于開(kāi)發(fā)者來(lái)說(shuō),這是調(diào)用鏈比較長(zhǎng)的安裝方式,能學(xué)到的更多。其他的安裝場(chǎng)景會(huì)在本系列的后續(xù)文章進(jìn)行講解。
3.尋找PackageInstaller入口
在Android7.0之前我們可以通過(guò)如下代碼安裝指定路徑中的APK。
Intent intent = new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setDataAndType(Uri.parse("file://" + path),"application/vnd.android.package-archive"); context.startActivity(intent);
但是Android7.0或更高版本再這么做,就會(huì)報(bào)FileUriExposedException異常。這是因?yàn)镾trictMode API 政策禁止應(yīng)用程序?qū)ile:// Uri暴露給另一個(gè)應(yīng)用程序,如果包含file:// Uri的 intent 離開(kāi)你的應(yīng)用,就會(huì)報(bào)FileUriExposedException 異常。為了解決這個(gè)問(wèn)題,谷歌提供了FileProvider,F(xiàn)ileProvider繼承自ContentProvider ,使用它可以將file://Uri替換為content://Uri,具體怎么使用FileProvider并不是本文的重點(diǎn),只要知道無(wú)論是Android7.0之前還是Android7.0以及更高版本,都會(huì)調(diào)用如下代碼:
Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(xxxxx, "application/vnd.android.package-archive");
Intent的Action屬性為ACTION_VIEW,Type屬性指定Intent的數(shù)據(jù)類(lèi)型為application/vnd.android.package-archive。
能隱式匹配的Activity為InstallStart,需要注意的是,這里分析的源碼基于Android8.0,7.0能隱式匹配的Activity為PackageInstallerActivity。
packages/apps/PackageInstaller/AndroidManifest.xml
<activity android:name=".InstallStart" android:exported="true" android:excludeFromRecents="true"> <intent-filter android:priority="1"> <action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.INSTALL_PACKAGE" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="file" /> <data android:scheme="content" /> <data android:mimeType="application/vnd.android.package-archive" /> </intent-filter> ... </activity>
InstallStart是PackageInstaller中的入口Activity,其中PackageInstaller是系統(tǒng)內(nèi)置的應(yīng)用程序,用于安裝和卸載應(yīng)用。當(dāng)我們調(diào)用PackageInstaller來(lái)安裝應(yīng)用時(shí)會(huì)跳轉(zhuǎn)到InstallStart,并調(diào)用它的onCreate方法:
packages/apps/PackageInstaller/src/com/android/packageinstaller/InstallStart.java
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { if (PackageInstaller.ACTION_CONFIRM_PERMISSIONS.equals(intent.getAction())) {//1 nextActivity.setClass(this, PackageInstallerActivity.class); } else { Uri packageUri = intent.getData(); if (packageUri == null) {//2 Intent result = new Intent(); result.putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_FAILED_INVALID_URI); setResult(RESULT_FIRST_USER, result); nextActivity = null; } else { if (packageUri.getScheme().equals(SCHEME_CONTENT)) {//3 nextActivity.setClass(this, InstallStaging.class); } else { nextActivity.setClass(this, PackageInstallerActivity.class); } } } if (nextActivity != null) { startActivity(nextActivity); } finish(); }
注釋1處判斷Intent的Action是否為CONFIRM_PERMISSIONS,根據(jù)本文的應(yīng)用情景顯然不是,接著往下看,注釋2處判斷packageUri 是否為空也不成立,注釋3處,判斷Uri的Scheme協(xié)議是否是content,如果是就跳轉(zhuǎn)到InstallStaging,如果不是就跳轉(zhuǎn)到PackageInstallerActivity。本文的應(yīng)用情景中,Android7.0以及更高版本我們會(huì)使用FileProvider來(lái)處理URI ,F(xiàn)ileProvider會(huì)隱藏共享文件的真實(shí)路徑,將路徑轉(zhuǎn)換成content://Uri路徑,這樣就會(huì)跳轉(zhuǎn)到InstallStaging。InstallStaging的onResume方法如下所示。
packages/apps/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
@Override protected void onResume() { super.onResume(); if (mStagingTask == null) { if (mStagedFile == null) { try { mStagedFile = TemporaryFileManager.getStagedFile(this);//1 } catch (IOException e) { showError(); return; } } mStagingTask = new StagingAsyncTask(); mStagingTask.execute(getIntent().getData());//2 } }
注釋1處如果File類(lèi)型的mStagedFile 為null,則創(chuàng)建mStagedFile ,mStagedFile用于存儲(chǔ)臨時(shí)數(shù)據(jù)。 注釋2處啟動(dòng)StagingAsyncTask,并傳入了content協(xié)議的Uri,如下所示。
packages/apps/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
private final class StagingAsyncTask extends AsyncTask<Uri, Void, Boolean> { @Override protected Boolean doInBackground(Uri... params) { if (params == null || params.length <= 0) { return false; } Uri packageUri = params[0]; try (InputStream in = getContentResolver().openInputStream(packageUri)) { if (in == null) { return false; } try (OutputStream out = new FileOutputStream(mStagedFile)) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = in.read(buffer)) >= 0) { if (isCancelled()) { return false; } out.write(buffer, 0, bytesRead); } } } catch (IOException | SecurityException e) { Log.w(LOG_TAG, "Error staging apk from content URI", e); return false; } return true; } @Override protected void onPostExecute(Boolean success) { if (success) { Intent installIntent = new Intent(getIntent()); installIntent.setClass(InstallStaging.this, PackageInstallerActivity.class); installIntent.setData(Uri.fromFile(mStagedFile)); installIntent .setFlags(installIntent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT); installIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); startActivityForResult(installIntent, 0); } else { showError(); } } } }
doInBackground方法中將packageUri(content協(xié)議的Uri)的內(nèi)容寫(xiě)入到mStagedFile中,如果寫(xiě)入成功,onPostExecute方法中會(huì)跳轉(zhuǎn)到PackageInstallerActivity中,并將mStagedFile傳進(jìn)去。繞了一圈又回到了PackageInstallerActivity,這里可以看出InstallStaging主要起了轉(zhuǎn)換的作用,將content協(xié)議的Uri轉(zhuǎn)換為File協(xié)議,然后跳轉(zhuǎn)到PackageInstallerActivity,這樣就可以像此前版本(Android7.0之前)一樣啟動(dòng)安裝流程了。
4.PackageInstallerActivity解析
從功能上來(lái)說(shuō),PackageInstallerActivity才是應(yīng)用安裝器PackageInstaller真正的入口Activity,PackageInstallerActivity的onCreate方法如下所示。
packages/apps/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
@Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); if (icicle != null) { mAllowUnknownSources = icicle.getBoolean(ALLOW_UNKNOWN_SOURCES_KEY); } mPm = getPackageManager(); mIpm = AppGlobals.getPackageManager(); mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); mInstaller = mPm.getPackageInstaller(); mUserManager = (UserManager) getSystemService(Context.USER_SERVICE); ... //根據(jù)Uri的Scheme進(jìn)行預(yù)處理 boolean wasSetUp = processPackageUri(packageUri);//1 if (!wasSetUp) { return; } bindUi(R.layout.install_confirm, false); //判斷是否是未知來(lái)源的應(yīng)用,如果開(kāi)啟允許安裝未知來(lái)源選項(xiàng)則直接初始化安裝 checkIfAllowedAndInitiateInstall();//2 }
首先初始話安裝所需要的各種對(duì)象,比如PackageManager、IPackageManager、AppOpsManager和UserManager等等,它們的描述如下表所示。
| 類(lèi)名 | 描述 |
|---|---|
| PackageManager | 用于向應(yīng)用程序進(jìn)程提供一些功能,最終的功能是由PMS來(lái)實(shí)現(xiàn)的 |
| IPackageManager | 一個(gè)AIDL的接口,用于和PMS進(jìn)行進(jìn)程間通信 |
| AppOpsManager | 用于權(quán)限動(dòng)態(tài)檢測(cè),在Android4.3中被引入 |
| PackageInstaller | 提供安裝、升級(jí)和刪除應(yīng)用程序功能 |
| UserManager | 用于多用戶(hù)管理 |
注釋1處的processPackageUri方法如下所示。
packages/apps/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
private boolean processPackageUri(final Uri packageUri) { mPackageURI = packageUri; final String scheme = packageUri.getScheme();//1 switch (scheme) { case SCHEME_PACKAGE: { try { ... } break; case SCHEME_FILE: { File sourceFile = new File(packageUri.getPath());//1 //得到sourceFile的包信息 PackageParser.Package parsed = PackageUtil.getPackageInfo(this, sourceFile);//2 if (parsed == null) { Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation"); showDialogInner(DLG_PACKAGE_ERROR); setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK); return false; } //對(duì)parsed進(jìn)行進(jìn)一步處理得到包信息PackageInfo mPkgInfo = PackageParser.generatePackageInfo(parsed, null, PackageManager.GET_PERMISSIONS, 0, 0, null, new PackageUserState());//3 mAppSnippet = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile); } break; default: { Log.w(TAG, "Unsupported scheme " + scheme); setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI); finish(); return false; } } return true; }
首先在注釋1處得到packageUri的Scheme協(xié)議,接著根據(jù)這個(gè)Scheme協(xié)議分別對(duì)package協(xié)議和file協(xié)議進(jìn)行處理,如果不是這兩個(gè)協(xié)議就會(huì)關(guān)閉PackageInstallerActivity并return false。我們主要來(lái)看file協(xié)議的處理,注釋1處根據(jù)packageUri創(chuàng)建一個(gè)新的File。注釋2處的內(nèi)部會(huì)用PackageParser的parsePackage方法解析這個(gè)File(這個(gè)File其實(shí)是APK文件),得到APK的包信息Package ,Package包含了該APK的所有信息。注釋3處會(huì)將Package根據(jù)uid、用戶(hù)狀態(tài)信息和PackageManager的配置等變量對(duì)包信息Package做進(jìn)一步處理得到PackageInfo。
回到PackageInstallerActivity的onCreate方法的注釋2處,checkIfAllowedAndInitiateInstall方法如下所示。
packages/apps/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
private void checkIfAllowedAndInitiateInstall() { //判斷如果允許安裝未知來(lái)源或者根據(jù)Intent判斷得出該APK不是未知來(lái)源 if (mAllowUnknownSources || !isInstallRequestFromUnknownSource(getIntent())) {//1 //初始化安裝 initiateInstall();//2 return; } // 如果管理員限制來(lái)自未知源的安裝, 就彈出提示Dialog或者跳轉(zhuǎn)到設(shè)置界面 if (isUnknownSourcesDisallowed()) { if ((mUserManager.getUserRestrictionSource(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, Process.myUserHandle()) & UserManager.RESTRICTION_SOURCE_SYSTEM) != 0) { showDialogInner(DLG_UNKNOWN_SOURCES_RESTRICTED_FOR_USER); return; } else { startActivity(new Intent(Settings.ACTION_SHOW_ADMIN_SUPPORT_DETAILS)); finish(); } } else { handleUnknownSources();//3 } }
注釋1處判斷允許安裝未知來(lái)源或者根據(jù)Intent判斷得出該APK不是未知來(lái)源,就調(diào)用注釋2處的initiateInstall方法來(lái)初始化安裝。如果管理員限制來(lái)自未知源的安裝, 就彈出提示Dialog或者跳轉(zhuǎn)到設(shè)置界面,否則就調(diào)用注釋3處的handleUnknownSources方法來(lái)處理未知來(lái)源的APK。注釋2處的initiateInstall方法如下所示。
packages/apps/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
private void initiateInstall() { String pkgName = mPkgInfo.packageName;//1 String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] { pkgName }); if (oldName != null && oldName.length > 0 && oldName[0] != null) { pkgName = oldName[0]; mPkgInfo.packageName = pkgName; mPkgInfo.applicationInfo.packageName = pkgName; } try { //根據(jù)包名獲取應(yīng)用程序信息 mAppInfo = mPm.getApplicationInfo(pkgName, PackageManager.MATCH_UNINSTALLED_PACKAGES);//2 if ((mAppInfo.flags&ApplicationInfo.FLAG_INSTALLED) == 0) { mAppInfo = null; } } catch (NameNotFoundException e) { mAppInfo = null; } //初始化安裝確認(rèn)界面 startInstallConfirm();//3 }
注釋1處得到包名,注釋2處根據(jù)包名獲取獲取應(yīng)用程序信息ApplicationInfo。注釋3處的startInstallConfirm方法如下所示。
packages/apps/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
private void startInstallConfirm() { //省略初始化界面代碼 ... AppSecurityPermissions perms = new AppSecurityPermissions(this, mPkgInfo);//1 final int N = perms.getPermissionCount(AppSecurityPermissions.WHICH_ALL); if (mAppInfo != null) { msg = (mAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ? R.string.install_confirm_question_update_system : R.string.install_confirm_question_update; mScrollView = new CaffeinatedScrollView(this); mScrollView.setFillViewport(true); boolean newPermissionsFound = false; if (!supportsRuntimePermissions) { newPermissionsFound = (perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0); if (newPermissionsFound) { permVisible = true; mScrollView.addView(perms.getPermissionsView( AppSecurityPermissions.WHICH_NEW));//2 } } ... }
startInstallConfirm方法中首先初始化安裝確認(rèn)界面,就是我們平常安裝APK時(shí)出現(xiàn)的界面,界面上有確認(rèn)和取消按鈕并會(huì)列出安裝該APK需要訪問(wèn)的系統(tǒng)權(quán)限。需要注意的是,不同廠商定制的Android系統(tǒng)會(huì)有不同的安裝確認(rèn)界面。
注釋1處會(huì)創(chuàng)建AppSecurityPermissions,它會(huì)提取出APK中權(quán)限信息并展示出來(lái),這個(gè)負(fù)責(zé)展示的View是AppSecurityPermissions的內(nèi)部類(lèi)PermissionItemView。注釋2處調(diào)用AppSecurityPermissions的getPermissionsView方法來(lái)獲取PermissionItemView,并將PermissionItemView添加到CaffeinatedScrollView中,這樣安裝該APK需要訪問(wèn)的系統(tǒng)權(quán)限就可以全部的展示出來(lái)了,PackageInstaller的初始化工作就完成了。
5.總結(jié)
現(xiàn)在來(lái)總結(jié)下PackageInstaller初始化的過(guò)程:
- 根據(jù)Uri的Scheme協(xié)議不同,跳轉(zhuǎn)到不同的界面,content協(xié)議跳轉(zhuǎn)到InstallStart,其他的跳轉(zhuǎn)到PackageInstallerActivity。本文應(yīng)用場(chǎng)景中,如果是Android7.0以及更高版本會(huì)跳轉(zhuǎn)到InstallStart。
- InstallStart將content協(xié)議的Uri轉(zhuǎn)換為File協(xié)議,然后跳轉(zhuǎn)到PackageInstallerActivity。
- PackageInstallerActivity會(huì)分別對(duì)package協(xié)議和file協(xié)議的Uri進(jìn)行處理,如果是file協(xié)議會(huì)解析APK文件得到包信息PackageInfo。
- PackageInstallerActivity中會(huì)對(duì)未知來(lái)源進(jìn)行處理,如果允許安裝未知來(lái)源或者根據(jù)Intent判斷得出該APK不是未知來(lái)源,就會(huì)初始化安裝確認(rèn)界面,如果管理員限制來(lái)自未知源的安裝, 就彈出提示Dialog或者跳轉(zhuǎn)到設(shè)置界面。
PackageInstaller的初始化就講到這,關(guān)于PackageInstaller的安裝APK的過(guò)程會(huì)在本系列的下一篇文章進(jìn)行講解。
posted on 2018-07-15 12:35 安卓筆記俠 閱讀(3156) 評(píng)論(0) 收藏 舉報(bào)
浙公網(wǎng)安備 33010602011771號(hào)