1. 程式人生 > >Android APK安裝與解除安裝機制

Android APK安裝與解除安裝機制

本文主要介紹Android系統如何實現APK安裝、解除安裝、更新等操作。主要內容包括以下內容:

  1. 安裝和解除安裝APK的方法有哪些,每種方法實現的原理是什麼?
  2. APK安裝和解除安裝過程中,系統資料發生了哪些變化?
  3. Android App端常用的Package Manager使用方法簡介。

注:本文是在本人閱讀<Android的設計與實現I>、<深入理解Android卷II>、以及網路中關於Android安裝方式的基礎上完成的。本人的主要工作在於閱讀相關部分的Android原始碼,並製作相應程式碼呼叫的時序圖;嘗試將所閱讀的文章中的安裝方法付諸實施。

1. APK安裝解除安裝方法及原理淺析

1.1 APK安裝方式

常見的APK安裝方式有以下幾種:

安裝方式 觸發時機 面向的使用者型別
系統自帶和廠商預裝 系統首次啟動 System Designer
adb命令安裝 使用adb push命令 Programmer
adb命令安裝 使用adb (shell pm) install命令 Programmer
網路下載應用安裝 下載完成之後安裝 User
第三方應用安裝(PackageInstaller.apk等) 通過SdCard中的APK安裝 User

1.2 APK安裝、解除安裝原理淺析

Android系統中,APK安裝、解除安裝的原理,與Windows系統軟體安裝(具體原理參考

軟體安裝原理MSI檔案簡介)的基本原理類似。

  • APK安裝可以歸結為以下幾個過程:
    1. 將APK檔案複製到指定的目錄下;
    2. 解壓APK,拷貝檔案,建立應用的資料目錄;
    3. 把dex檔案(Dalvik位元組碼)儲存到dalvik-cache目錄;
    4. 解析APK的AndroidManifest.xml檔案,將其中宣告的元件等資訊儲存在PMS中;更新PMS中相應的資料結構。
    5. 其他操作;
  • APK解除安裝:將APK檔案、data/data/pacakgeName/、dalvik-cache目錄中相應的檔案刪除;並更新PMS中相應的資料結構。

1.3 APK幾種安裝方式異同點

  1. 系統自帶的應用以及廠商預裝的應用等,在手機首次啟動時,會通過掃描/system/app/、/system/framework/、/vendor/app/等目錄下面的APK檔案,完成安裝;如果是Android的原生系統,或者cm等系統,則沒有/vendor/app/目錄。
  2. 通過adb push命令可以將APK檔案推送到/system/app/、/system/framework/、/vendor/app/、/data/app/等目錄下面,然後再次啟動手機裝置,便可以完成安裝過程。具體命令如下:
    adb root
     adb push flyflow-release.apk /system/app
     adb reboot
    使用該方式安裝APK,具有以下特點:
    • 使用這種方式安裝APK時,要求手機被完全root,可以通過PC端獲取root許可權。
    • 如果將APK檔案推送到/system/app/、/system/framework/、/vendor/app/目錄下,該App便成為系統預裝App,沒有root許可權不能解除安裝該應用。
    • 使用該種方式安裝的APK,於系統自帶的App以及廠商預裝的App安裝方式一樣,也是通過PMS掃描相應的資料夾完成。
  3. 使用adb install命令安裝APK。常用的命令如下:
    adb install flyflow-release.apk
    adb shell pm install flyflow-release.apk
    這兩種命令最終都會呼叫Android端的/bin/pm的install命令完成安裝。
  4. 網路應用下載安裝APK之後,會呼叫PackageManager的installPackage(*)方法完成安裝工作;該方法最終會呼叫PackageManagerService中的installPackage來完成APK安裝。可通過以下形式實現驗證: a) 將APK檔案實現放入sdcard中; b) 設計一個Android InstallDemo,包含一個按鈕,點選按鈕便會呼叫installPackage方法進行安裝。其中installPackage安裝的檔案為步驟a中的APK; 注意事項有:

    • 該實驗在安裝的過程中,要求InstallDemo工程具有INSTALL_PACKAGES許可權。
  5. 使用PackageInstaller.apk進行安裝時,其啟動PackageInstallerActivity來完成安裝包解析;在解析完成並得到使用者的安裝確認之後,啟動 InstallAppProgress並呼叫安裝介面pm.installPackage進行安裝。(該方式未進行深入調研,道聽途說)

2. PMS安裝APK的過程解析

本節詳細介紹APK的幾種安裝方式,主要包括開機掃描安裝、adb install命令安裝等。

2.1 掃描安裝過程解析

Android裝置在啟動的過程中,會掃描本地安裝的所有的App。整個掃描過程可以通過以下流程圖來說明: APK開機掃描安裝掃描安裝過程分析如下:

  1. 在PackageManagerService建構函式中,掃描各個APK目錄之前會建立一個Settings物件mSettings。其中Settings類負責讀取系統的配置資訊,主要解析的檔案有packages.xml、packages-stopped.xml等檔案。會將解析的資訊分別存入mPackages、mRenamedPackages、mDisableSysPackages等資料結構中。重要的函式呼叫有:
    mRestoredSettings = mSettings.readLPw(this, sUserManager.getUsers(false),mSdkVersion, mOnlyCore);
  2. 開機啟動時會以此掃描目錄:/system/app、/system/framework、 /vendor/app、/data/app等。具體呼叫有: APK目錄掃描呼叫
  3. 掃描每個APK目錄的核心程式碼如下:
    // scanDirLI(**)
    for (File file : files) {
      final boolean isPackage = (isApkFile(file) || file.isDirectory())
              && !PackageInstallerService.isStageName(file.getName());
      if (!isPackage) { // Ignore entries which are not packages
           continue;
       }
       try {
           scanPackageLI(file, parseFlags | PackageParser.PARSE_MUST_BE_APK,
                   scanFlags, currentTime, null);
       } catch (PackageManagerException e) {
           // exception process
           .....
    }
    
  4. scanPackageLI(File scanFile,...)該函式負責完成APK的掃描工作,APK的掃描工作具體有PackageParser.parsePackage(*)完成。在掃描過程中,會解析AndroidManifiest.xml檔案等資訊,並將掃描結果存放在PackageParser.Package物件中。
  5. 在完成包的資訊解析之後需要完成包資訊同步工作,主要因為:scanPackageLI掃描到的APK可能是已經更名的包、disable的包、需要升級的包、已經安裝並且簽名衝突的包、被非系統級包替代系統包的情況,需要對這些情況一一處理,保證資訊的正確性
  6. 如果Package需要Rename或者Update則會進行簽名比較,以防止簽名不一致的情況;
  7. 最終會呼叫scanPackageLI(PackageParser.Package,....)函式完成實際的Package安裝或更新操作。
  8. 在掃描過程中,Package的Rename、Update、Install操作都是由scanPackage(PackageParser,int,int,long,UserHandler)來完成.該函式會呼叫scanPackageDirtyLI(...)完成具體的操作。scanPackageDirtyLI函式在後續章節會分析。

2.2 adb命令安裝過程解析

2.2.1 adb install命令

adb命令的intall命令在commandline.c中對應的實現函式為install_app(....),該函式的程式碼有如下呼叫:

if (!(err = do_sync_push(filename, to, 1 /* verify APK */))) {
        /* file in place; tell the Package Manager to install it */
        argv[argc - 1] = to;      
        /* destination name, not source location */
        pm_command(transport, serial, argc, argv);
        delete_file(transport, serial, to);
}
pm_command(...)函式實現如下:

static int pm_command(transport_type transport, char* serial,
                      int argc, char** argv)
{
    char buf[4096];
    snprintf(buf, sizeof(buf), "shell:pm");
    while(argc-- > 0) {
        char *quoted;
        quoted = dupAndQuote (*argv++);
        strncat(buf, " ", sizeof(buf)-1);
        strncat(buf, quoted, sizeof(buf)-1);
        free(quoted);
    }
    send_shellcommand(transport, serial, buf);
    return 0;
}

pm_command通過最終通過send_shellcommand(...)將資料傳送到手機端的adbd守護程序中;adbd在收到PC的Console發來的資料之後,會啟動一個Shell,然後執行pm。

2.2.2 adb shell pm命令

Android裝置端pm命令位於/system/bin目錄下,其是一個指令碼,具體內容如下:

#!/system/bin/sh
base=/system
export CLASSPATH=$base/framework/pm.jar
exec app_process $base/bin com.android.commands.pm.Pm "[email protected]"
1. 從上面指令碼可以看出,pm命令通過app_process執行pm.jar包的main函式;
2. Android系統中常用的monkey、pm、am等指令碼都是使用該方式來執行;
3. pm.jar對應的類為com.android.commands.pm.Pm,具體的安裝函式由runInstall來實現。該函式解析剩餘的install命令引數,並最終呼叫以下程式碼完成安裝:
// Populate verificationURI, optionally present
 final String verificationFilePath = nextArg();
 if (verificationFilePath != null) {
     System.err.println("\tver: " + verificationFilePath);
     verificationURI = Uri.fromFile(new File(verificationFilePath));
 } else {
     verificationURI = null;
 }

 LocalPackageInstallObserver obs = new LocalPackageInstallObserver();
 try {
     VerificationParams verificationParams = new VerificationParams(verificationURI,originatingURI, 
        referrerURI, VerificationParams.NO_UID, null);
     // 呼叫PMS完成安裝
       mPm.installPackageAsUser(apkFilePath, obs.getBinder(), installFlags,
             installerPackageName, verificationParams, abi, userId);
      // 安裝結果後續處理
    ....
 } catch (RemoteException e) {
     System.err.println(e.toString());
     System.err.println(PM_NOT_RUNNING_ERR);
     return 1;
 }

2.3 PMS中相關程式碼分析

adb安裝命令最終呼叫PMS中的installPackageAsUser函式進行APK安裝。整個過程的時序圖如下圖所示: adb命令安裝過程時序圖安裝APK的過程中具有以下特徵:

  • installPackageAsUser函式檢查客戶端程序是否具有安裝Package的許可權,其中Shell和root程序都是具有該許可權的。
  • installPackageAsUser函式會向PackageHandler傳送一個INIT_COPY的訊息,由PackageHandler來完成APK安裝;
  • PackageHandler會呼叫InstallParms中的handleStartCopy()和handleReturnCode函式完成相應的操作。
  • 在handleStartCopy函式裡完成的事情有:
    • 根據adb install的引數,判斷安裝位置;
    • 呼叫DeviceStorageMonitorService判斷是否有足夠的空間完成APK安裝,並給出APK推薦的安裝路徑;
    • 建立一個安裝引數(如果安裝在內部儲存空間時,該引數是一個FileInstallArgs物件)以便後續安裝使用;
    • 呼叫InstallArgs的copyApk函式將APK從臨時目錄複製到指定的目錄中;
  • 通過adb命令安裝APK時,最終也是呼叫scanPackageDirLI函式完成APK安裝。
  • 在該過程中,會判斷是新安裝一個APK還是覆蓋更新安裝APK。針對不同的情況會進行特殊的處理。
  • 在安裝完成之後,processPendingInstall函式會想PackageHandler傳送一個POST_INSTALL訊息。然後在該訊息的處理中,會根據APK的安裝情況,傳送相應的廣播資訊。

3. APK安裝核心函式ScanPackageDirtyLI函式分析

從上述過程可以看出,不論是開機掃描安裝APK,還是通過adb命令安裝APK,最終都會呼叫scanPackageDirtyLI函式進行APK安裝。該函式的流程圖如下圖所示: scanPackageDirtyLI流程圖 該函式完成的主要工作有:

  • 初始化Package的資料目錄和資源目錄;
  • 如果需要則更新已有的Package的共享程式碼庫;
  • 如果安裝時傳遞了簽名信息,則驗證簽名信息的合法性;
  • 驗證新安裝的APK中的Provider是否與系統中現有的Provider有衝突,並進行相應的處理;
  • 如果新安裝APK需要使用其他Package的許可權,則進行相應處理;
  • 呼叫createDataDirsLI()安裝APK;
  • 設定本地lib路徑;
  • 安裝成功之後將Package資訊更新到PMS和Setting相應的資料結構中;
  • 設定APK安裝時間;
  • 設定APK的Provider資訊,將Provider新增到相應的資料結構中;
  • 設定許可權組和許可權資訊;
  • 該函式的主要工作便是將安裝的APK的資訊新增到PMS中,比如講Provider、Activity、Service、Receiver等元件資訊新增到相應的資料結構中,以便其他函式能夠查詢到。
  • 在該函式中還對framework-res.apk進行特殊的資訊處理。framework-res.apk中主要包含以下資訊:
    • 幾個常用的Activity:ChooserActivity、ShutdownActivity、RingtonePckerActivity;
    • framework-res.apk與PMS聯絡緊密,其中PMS中的mPlatformPackage成員儲存該Package資訊;mAndroidApplicatioin儲存該Package的ApplicationInfo資訊;mResolveActivity表示ChooserActivity資訊的ActivityInfo;mResolveInfo儲存系統解析的Intent後得到的結果資訊。

3.2 createDataDirsLI分析

上個小節已經說明,scanPackageDirLI函式會呼叫createDataDirsLI的函式來完成安裝。該函式主要做了兩件事情:

  • 呼叫mInstaller.install()函式完成APK安裝;
  • 呼叫mInstaller.createUserData()函式建立使用者資訊。 其中mInstaller是在PMS.main()函式中傳遞進來的Installer物件。該函式程式碼如下:
    private int createDataDirsLI(String packageName, int uid, String seinfo) {
          int[] users = sUserManager.getUserIds();
          int res = mInstaller.install(packageName, uid, uid, seinfo);
          if (res < 0) {
              return res;
          }
          for (int user : users) {
              if (user != 0) {
                  res = mInstaller.createUserData(packageName,
                          UserHandle.getUid(user, uid), user, seinfo);
                  if (res < 0) {
                      return res;
                  }
              }
          }
          return res;
    }

3.3 Installer介紹

通過Installer類中install函式程式碼如下:

public int install(String name, int uid, int gid, String seinfo) {
    StringBuilder builder = new StringBuilder("install");
    builder.append(' ');
    builder.append(name);
    builder.append(' ');
    builder.append(uid);
    builder.append(' ');
    builder.append(gid);
    builder.append(' ');
    builder.append(seinfo != null ? seinfo : "!");
    return mInstaller.execute(builder.toString());
}
分析得出以下結論: 1. Installer.install()函式和createUserData()進行完成了命令組裝工作,在組裝完命令之後,將命令傳遞給InstallerConnection處理。 通過分析InstallerConnection.java得到以下結論: 1. InstallerConnection連線一個名為Installd的服務 2. Install具體的命令有Installd完成。 其中InstallerConnnection.connect()函式程式碼如下:
private boolean connect() {
        if (mSocket != null) {
            return true;
        }
        Slog.i(TAG, "connecting...");
        try {
            mSocket = new LocalSocket();
            LocalSocketAddress address = new LocalSocketAddress("installd",
                    LocalSocketAddress.Namespace.RESERVED);
            mSocket.connect(address);
            mIn = mSocket.getInputStream();
            mOut = mSocket.getOutputStream();
        } catch (IOException ex) {
            disconnect();
            return false;
        }
        return true;
}

3.4 Installed介紹

Installd是一個native程序,該程序啟動一個socket,然後處理來之Installer的命令。Installd的實現原理可以參考博文Android安裝服務installd原始碼分析。 installd原始碼位於frameworks/base/cmds/installd目錄下,其中install操作對應的原始碼在frameworks/base/cmds/installd/commands.c中,具體程式碼如下:

int install(const char *pkgname, uid_t uid, gid_t gid)
{
    char pkgdir[PKG_PATH_MAX];//程式目錄路徑最長為256
    char libdir[PKG_PATH_MAX];//程式lib路徑最長為256
    //許可權判斷
    if ((uid < AID_SYSTEM) || (gid < AID_SYSTEM)) {
        ALOGE("invalid uid/gid: %d %d\n", uid, gid);
        return -1;
    }
    //組合應用程式安裝目錄pkgdir=/data/data/應用程式包名
    if (create_pkg_path(pkgdir, pkgname, PKG_DIR_POSTFIX, 0)) {
        ALOGE("cannot create package path\n");
        return -1;
    }
    //組合應用程式庫目錄libdir=/data/data/應用程式包名/lib
    if (create_pkg_path(libdir, pkgname, PKG_LIB_POSTFIX, 0)) {
        ALOGE("cannot create package lib path\n");
        return -1;
    }
    //建立目錄pkgdir=/data/data/應用程式包名
    if (mkdir(pkgdir, 0751) < 0) {
        ALOGE("cannot create dir '%s': %s\n", pkgdir, strerror(errno));
        return -errno;
    }
    //修改/data/data/應用程式包名目錄的許可權
    if (chmod(pkgdir, 0751) < 0) {
        ALOGE("cannot chmod dir '%s': %s\n", pkgdir, strerror(errno));
        unlink(pkgdir);
        return -errno;
    }
    //建立目錄libdir=/data/data/應用程式包名/lib
    if (mkdir(libdir, 0755) < 0) {
        ALOGE("cannot create dir '%s': %s\n", libdir, strerror(errno));
        unlink(pkgdir);
        return -errno;
    }
    //修改/data/data/應用程式包名/lib目錄的許可權
    if (chmod(libdir, 0755) < 0) {
        ALOGE("cannot chmod dir '%s': %s\n", libdir, strerror(errno));
        unlink(libdir);
        unlink(pkgdir);
        return -errno;
    }
    //修改/data/data/應用程式包名目錄的所有許可權
    if (chown(libdir, AID_SYSTEM, AID_SYSTEM) < 0) {
        ALOGE("cannot chown dir '%s': %s\n", libdir, strerror(errno));
        unlink(libdir);
        unlink(pkgdir);
        return -errno;
    }
    //修改/data/data/應用程式包名/lib目錄的所有許可權
    if (chown(pkgdir, uid, gid) < 0) {
        ALOGE("cannot chown dir '%s': %s\n", pkgdir, strerror(errno));
        unlink(libdir);
        unlink(pkgdir);
        return -errno;
    }
    return 0;
}

4. APK的解除安裝

從Installd的實現中,我們可以看到unInstall操作其實就是刪除相應的資料檔案和資原始檔。unInstall的具體實現如下:

int uninstall(const char *pkgname, uid_t persona)
{
    char pkgdir[PKG_PATH_MAX];
    if (create_pkg_path(pkgdir, pkgname, PKG_DIR_POSTFIX, persona))
        return -1;
    /* delete contents AND directory, no exceptions */
    return delete_dir_contents(pkgdir, 1, NULL);
}

5. 待完成的任務

  1. PackageInstaller.apk等第三方App如何實現APK安裝;
  2. 如何重新命名一個APK,以及更換APK的ICON;
  3. 通過adb push命令將APK安裝包推送到/system/app能否實時觸發APK安裝;
  4. APK安裝完成之後,是否能立即查詢到該APK資訊;解除安裝完成之後,是否可立即判斷當前的包已經被解除安裝;
  5. 製作Demo完成APK安裝,使用命令完成APK安裝;
  6. 如何通過APK安裝/解除安裝的廣播進行相關程式設計;

6.小結

  1. adb的實現機制
  2. Content Provider命名、自定義Permission命名等需要保證全域性唯一;
  3. 安裝、解除安裝完成會發送廣播;
  4. Pm、monkey、Am指令碼的實現機制;
  5. 可以通過adb push或者直接將APK放入到/system/app、/vendor/app完成預置操作;
  6. 如果遇到安裝失敗的情況,終極方案直接刪除相應的檔案,並重啟手機;
  7. 一臺PC最多同時連線16個device/emulator;
  8. /data/data//該檔案的許可權為751; /data/data//lib該目錄的許可權為755,說明每個App的lib是所有使用者可讀可執行的。

7.參考文獻