天天看點

Android資源管理中的Runtime Resources Overlay-------之PMS、installd和idmap的處理(二)

        前面我們對RRO(Runtime Resources Overlay)的使用做了介紹,并且知道了它大概的原理。下面我們詳細介紹其實作過程,先說兩個名詞:

        target包:就是要被覆寫的包

        overlay包:就是我們的資源包或者主題包等

PMS的處理

        PMS的處理比較簡單,它會在開機的時候對所有的包做掃描,提取出包資訊,如果有RRO對應的overlay包和target包,則會以local_socket的形式請求installd去做idmap。

        先看PackageManagerService.java中的一些資料結構:

/**
 *這個常量就是系統定義的overlay package存放的路徑,也就是前面我們要把demo裡的overlay package push
 *到/vendor/overlay的原因。
 */
private static final String VENDOR_OVERLAY_DIR = "/vendor/overlay";

//mOverlays用來存儲overlay相關的包資訊
final ArrayMap<String, ArrayMap<String, PackageParser.Package>> mOverlays =
        new ArrayMap<String, ArrayMap<String, PackageParser.Package>>();
           

        mOverlays的key是target包的包名。由于一個target包可以被多個overlay包覆寫,是以它的vaule是一個ArrayMap:key表示overlay package的packageName,value則是overlay package。這裡就有個問題:如果一個target package被多個overlay package覆寫了,那麼到底哪個overlay package 生效呢?這個我們可以通過

android:priority="1"

來指定overlay package 的優先級,數字越大,優先級越高,取值範圍0~9999。

        我們知道在PackageManagerService構造的時候就回去掃描系統一些目錄。這當中,第一個掃描的就是/vendor/overlay目錄,解析其AndroidManifest.xml檔案,拿到它的target package和priority。我們看看frameworks/base/core/java/android/content/pm/PackageParser.java中的相關實作:

private Package parseBaseApk(Resources res, XmlResourceParser parser, int flags,
            String[] outError) throws XmlPullParserException, IOException {
            
    //......省略無關代碼
    
    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
        if (tagName.equals("application")) {
             //解析application相關屬性
             //.........非我們關注的重點,省略之
        }else if (tagName.equals("overlay")) {
                //解析我們的overlay标簽
                pkg.mTrustedOverlay = trustedOverlay;

                sa = res.obtainAttributes(attrs,
                        com.android.internal.R.styleable.AndroidManifestResourceOverlay);
                //擷取android:target屬性值
                pkg.mOverlayTarget = sa.getString(
                        com.android.internal.R.styleable.AndroidManifestResourceOverlay_targetPackage);
                //擷取android:priority屬性值
                pkg.mOverlayPriority = sa.getInt(
                        com.android.internal.R.styleable.AndroidManifestResourceOverlay_priority,
                        -1);
                sa.recycle();

                if (pkg.mOverlayTarget == null) {
                    outError[0] = "<overlay> does not specify a target package";
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return null;
                }
                //android:priority屬性值必須在0~9999之間
                if (pkg.mOverlayPriority < 0 || pkg.mOverlayPriority > 9999) {
                    outError[0] = "<overlay> priority must be between 0 and 9999";
                    mParseError =
                        PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return null;
                }
                XmlUtils.skipCurrentTag(parser);
        }
    }
}
           

        在包掃描的過程中會調用到PackageManagerServices的這個方法:

private PackageParser.Package scanPackageDirtyLI(PackageParser.Package pkg, int parseFlags,
            int scanFlags, long currentTime, UserHandle user) throws PackageManagerException {
    //..............省略無關代碼
    //pkg就是前面掃描過的overlay package,是以pkg.mOverlayTarget肯定不空
    if (pkg.mOverlayTarget != null) {//true
        //overlay 包的處理邏輯
        // This is an overlay package.
        if (pkg.mOverlayTarget != null && !pkg.mOverlayTarget.equals("android")) {
            //由于是第一次掃描,肯定會走進這個if,如果後面再有這個target包的overlay package則跳過
            if (!mOverlays.containsKey(pkg.mOverlayTarget)) {
                 mOverlays.put(pkg.mOverlayTarget,
                       new ArrayMap<String, PackageParser.Package>());
            }
            
            //将正在掃描的這個overlay package添加進去
            ArrayMap<String, PackageParser.Package> map = mOverlays.get(pkg.mOverlayTarget);
            map.put(pkg.packageName, pkg);
            PackageParser.Package orig = mPackages.get(pkg.mOverlayTarget);
            
            /**
             *orig為空,是以執行不到createIdmapForPackagePairLI方法
             *但是因為我們做了動态idmap或者線程排程原因,如果此時我們的target包已經掃描完,那
             *就得在這裡做idmap了,否則後面就沒有機會做了。
             */
            if (orig != null && !createIdmapForPackagePairLI(orig, pkg)) {
                        throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
                                "scanPackageLI failed to createIdmap");
            }
        }
    } else if (mOverlays.containsKey(pkg.packageName) &&
              !pkg.packageName.equals("android")) {//false 但是這個條件在掃描target package的時候會走到
        //target包的處理邏輯      
        // This is a regular package, with one or more known overlay packages.
        Slog.d(TAG, "idmapidmap, " + pkg.packageName + " is a regular package");
        createIdmapsForPackageLI(pkg);
    }
    //.........省略無關代碼
    return pkg;
}
           

        這裡我們看到過濾掉了android系統資源包framework-res.apk,因為系統資源包比較特殊,它的RRO是在AssetManager的native層處理的,這裡就不需要處理了。另外,當掃描完/vendor/overlay/目錄下的所有overlay包後,會掃描其它包,這個時候就會走到target package的處理邏輯,去做idmap:

private void createIdmapsForPackageLI(PackageParser.Package pkg) {
        //pkg是target package
        ArrayMap<String, PackageParser.Package> overlays = mOverlays.get(pkg.packageName);
        if (overlays == null) {
            Slog.w(TAG, "Unable to create idmap for " + pkg.packageName + ": no overlay packages");
            return;
        }
        for (PackageParser.Package opkg : overlays.values()) {
            // Not much to do if idmap fails: we already logged the error
            // and we certainly don't want to abort installation of pkg simply
            // because an overlay didn't fit properly. For these reasons,
            // ignore the return value of createIdmapForPackagePairLI.
            createIdmapForPackagePairLI(pkg, opkg);
        }
    }
           

        找到target package的所有overlay package,然後一個一個去做idmap。這裡不太關心做idmap的結果是成功了還是失敗了,如果失敗,我們就當沒有那個overlay package就完了,是以不處理createIdmapForPackagePairLI方法的傳回值。

private boolean createIdmapForPackagePairLI(PackageParser.Package pkg,
            PackageParser.Package opkg) {
        //先做相關檢查
        if (!opkg.mTrustedOverlay) {
            Slog.w(TAG, "Skipping target and overlay pair " + pkg.baseCodePath + " and " +
                    opkg.baseCodePath + ": overlay not trusted");
            return false;
        }
        //拿到一個target包的所有overlay包
        ArrayMap<String, PackageParser.Package> overlaySet = mOverlays.get(pkg.packageName);
        if (overlaySet == null) {
            Slog.e(TAG, "was about to create idmap for " + pkg.baseCodePath + " and " +
                    opkg.baseCodePath + " but target package has no known overlays");
            return false;
        }
        final int sharedGid = UserHandle.getSharedAppGid(pkg.applicationInfo.uid);
        // 告訴installer去做idmap
        if (mInstaller.idmap(pkg.baseCodePath, opkg.baseCodePath, sharedGid) != 0) {
            Slog.e(TAG, "Failed to generate idmap for " + pkg.baseCodePath + " and "
                    + opkg.baseCodePath);
            return false;
        }
        PackageParser.Package[] overlayArray =
            overlaySet.values().toArray(new PackageParser.Package[0]);
        //按優先級排序!
        Comparator<PackageParser.Package> cmp = new Comparator<PackageParser.Package>() {
            public int compare(PackageParser.Package p1, PackageParser.Package p2) {
                return p1.mOverlayPriority - p2.mOverlayPriority;
            }
        };
        Arrays.sort(overlayArray, cmp);
        //把所有overlay package的路徑寫入target package的applicationInfo
        pkg.applicationInfo.resourceDirs = new String[overlayArray.length];
        int i = 0;
        for (PackageParser.Package p : overlayArray) {
            pkg.applicationInfo.resourceDirs[i++] = p.baseCodePath;
        }
        return true;
    }
           

        這裡我們可以看到:PMS調用了mInstaller的idmap方法去做idmap,然後還會對同一個target package的所有overlay package按照優先級排序,最後還會把所有overlay package的路徑寫入target package的ApplicationInfo的resourceDirs裡面去。在Android資源管理中的SharedLibrary和Dynamic Reference-------之資源共享庫(一)我們對ApplicationInfo的resourceDirs變量有過說明,不知道大家是否還記得。另外,mInstaller是Installer類的執行個體,我們看看它的實作:

//frameworks/base/services/core/java/com/android/server/pm/Installer.java
public final class Installer extends SystemService {
    //......省略無關代碼
    private final InstallerConnection mInstaller;
    //......省略無關代碼
    public Installer(Context context) {
        super(context);
        mInstaller = new InstallerConnection();
    }
    //......省略無關代碼
    public int idmap(String targetApkPath, String overlayApkPath, int uid) {
        StringBuilder builder = new StringBuilder("idmap");
        builder.append(' ');
        builder.append(targetApkPath);
        builder.append(' ');
        builder.append(overlayApkPath);
        builder.append(' ');
        builder.append(uid);
        return mInstaller.execute(builder.toString());
    }
}
           

        Installer的實作非常簡單,包裝了InstallerConnection的一個執行個體,讓它去執行一條指令:idmap targetApkPath overlayApkPath uid

public class InstallerConnection {
        private InputStream mIn;
        private OutputStream mOut;
        //這裡有一個socket,用于跨程序和installd通信
        private LocalSocket mSocket;

        private final byte buf[] = new byte[1024];
        
        public int execute(String cmd) {
            String res = transact(cmd);
            try {
                return Integer.parseInt(res);
            } catch (NumberFormatException ex) {
                return -1;
            }
        }

        public synchronized String transact(String cmd) {
            //檢查連接配接狀态,如果沒有連接配接,則去做連接配接動作
            if (!connect()) {
                Slog.e(TAG, "connection failed");
                return "-1";
            }
            //給對端也就是installd發指令,如果不成功會做二次嘗試
            if (!writeCommand(cmd)) {
                if (!connect() || !writeCommand(cmd)) {
                    return "-1";
                }
            }
            if (LOCAL_DEBUG) {
                Slog.i(TAG, "send: '" + cmd + "'");
            }
            //讀取installd執行的結果并傳回
            final int replyLength = readReply();
            if (replyLength > 0) {
                String s = new String(buf, 0, replyLength);
                return s;
            }else {
                return "-1";
            }
        }
        private boolean connect() {
            //已經連接配接
            if (mSocket != null) {
                return true;
            }
            Slog.i(TAG, "connecting...");
            try {
                //連接配接installd
                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;
        }
        //往installd那裡發資料
        private boolean writeCommand(String cmdString) {
            final byte[] cmd = cmdString.getBytes();
            final int len = cmd.length;
            if ((len < 1) || (len > buf.length)) {
                return false;
            }

            buf[0] = (byte) (len & 0xff);
            buf[1] = (byte) ((len >> 8) & 0xff);
            try {
                mOut.write(buf, 0, 2);
                mOut.write(cmd, 0, len);
            } catch (IOException ex) {
                Slog.e(TAG, "write error");
                disconnect();
                return false;
            }
            return true;
        }
    }
           

        InstallerConnection的工作就是建立socket,向installd發送指令,然後讀取installd傳回的結果。到這裡,對于生成idmap檔案而言,PMS和Installer的工作已經完成,剩下的就交給installd這個守護程序了。

installd的處理

        installd程序在init程序解析init.rc的時候就會起來,它主要負責我們安裝包的管理,比如install、uninstall、rename、dexopt,當然還有idmap。我們來看看它的具體實作:

//frameworks/native/cmds/installd/installd.c
//沒什麼好說的,直接看idmap函數了
static int do_idmap(char **arg, char reply[REPLY_MAX])
{
    ALOGD("do_idmap : %s %s %s", arg[0], arg[1], arg[2]);
    return idmap(arg[0], arg[1], atoi(arg[2]));
}


//frameworks/native/cmds/installd/commands.c
int idmap(const char *target_apk, const char *overlay_apk, uid_t uid)
{
    ALOGV("idmap target_apk=%s overlay_apk=%s uid=%d\n", target_apk, overlay_apk, uid);

    int idmap_fd = -1;
    char idmap_path[PATH_MAX];

   //.....路徑、uid、權限等的檢查,如果不通過直接傳回,代碼就不貼了

    pid_t pid;
    //fork出子程序
    pid = fork();
    if (pid == 0) {
        //.........子程序,又是一些列的檢查,代碼不貼

        //在這裡做idmap
        run_idmap(target_apk, overlay_apk, idmap_fd);
        exit(1); /* only if exec call to idmap failed */
    } else {
        //父程序,等結果
        int status = wait_child(pid);
        if (status != 0) {
            ALOGE("idmap failed, status=0x%04x\n", status);
            goto fail;
        }
    }

    close(idmap_fd);
    return 0;
fail:
    if (idmap_fd >= 0) {
        close(idmap_fd);
        unlink(idmap_path);
    }
    return -1;
}

static void run_idmap(const char *target_apk, const char *overlay_apk, int idmap_fd)
{
    //idmap的bin檔案在/system/bin/idmap
    static const char *IDMAP_BIN = "/system/bin/idmap";
    static const size_t MAX_INT_LEN = 32;
    char idmap_str[MAX_INT_LEN];

    snprintf(idmap_str, sizeof(idmap_str), "%d", idmap_fd);
    //執行bin檔案
    execl(IDMAP_BIN, IDMAP_BIN, "--fd", target_apk, overlay_apk, idmap_str, (char*)NULL);
    ALOGE("execl(%s) failed: %s\n", IDMAP_BIN, strerror(errno));
}
           

        installd的工作也很簡單,做一系列的權限檢查,然後fork出子程序,加載/system/bin/idmap并執行。

idmap的處理

        idmap也是一個native的bin檔案,在/system/bin/下面,主要負責idmap檔案的生成和dump。我們可以通過adb shell進去,然後輸入idmap --help檢視它的用法:

Android資源管理中的Runtime Resources Overlay-------之PMS、installd和idmap的處理(二)

        --fd 建立idmap,直接輸出到對應的fd

        --path 建立idmap,輸出到指定的路徑

        --inspect dump idmap檔案

        --scan 用于一個target package 有多個overlay package的情況:

                dir-to-scan 多個overlay package所在的目錄

                target-to-look-for target包的packageName

                target target包的路徑

                 dir-to-hold-idmaps 存放生成的idmap檔案的目錄

        是以,我們可以通過idmap指令手動做idmap,直接把idmap檔案生成到/data/resource-cache/目錄下,效果跟通過PMS和installd來生成是一樣的。另外,我們可以通過idmap --inspect來檢視生成的idmap檔案的内容,這裡貼上一個我們可以先感受下:

Android資源管理中的Runtime Resources Overlay-------之PMS、installd和idmap的處理(二)

        IDMAP HEADER主要記錄了target包和overlay包的路徑:/system/priv-app/SystemUI/SystemUI.apk和/vendor/overlay/SystemUIOverlay.apk;

        要覆寫的type的個數:types count=1,因為這裡隻覆寫了drawable一個type;

        然後是每一個type的idmap資訊了,這個例子中隻有drawable一個type

        target type和overlay type的值都是0x00000002,說明drawable在target包和overlay包的id都是0x00000002;

        entry offset 0x00000039表示從target包的索引值為0x00000039的那個drawable開始做idmap;

        entry count 0x00000143表示drawable類型的資源,做了0x00000143個;

        這張圖我截得不全,因為它太大了。其實這個overlay pacakge僅僅有四張圖檔,也就是說隻需要覆寫4個drawable就可以了,但是為什麼entry count 不是0x00000004而是0x00000143呢?

        假設我們的target 包裡有10個drawable資源,它們的名稱為drawable0~drawable9,id為0x7f020000~0x7f020009;overlay包裡有3個資源它們的名稱分别是drawable4、drawable5、drawable8,id分别是0x7f030000、0x7f030001、0x7f030002。這樣就表示overlay包要覆寫target包裡的drawable4、drawable5、drawable8三個資源。那麼它生成的idmap的主要内容應該如下:

        target type 0x00000002

        overlay type 0x00000003

        entry offset 0x00000004

        entry count 0x00000006

        entry 0x00000000 drawable/drawable4

        entry 0x00000001 drawable/drawable5

        entry 0xffffffff drawable/drawable6

        entry 0xffffffff drawable/drawable7

        entry 0x00000002 drawable/drawable8

        entry 0xffffffff drawable/drawable9

        為什麼會是這個樣子呢?我們将在下一篇中介紹,這裡我們知道是這個樣子就可以了。下面我們看看idmap這個bin檔案的部分代碼吧,在frameworks/base/cmds/idmap/目錄下:

//frameworks/base/cmds/idmap/idmap.cpp
int main(int argc, char **argv)
{
    //.........省略無關代碼
    //接installd,這裡走--fd
    if (argc == 5 && !strcmp(argv[1], "--fd")) {
        return maybe_create_fd(argv[2], argv[3], argv[4]);
    }
    //.........省略無關代碼
}

int maybe_create_path(const char *target_apk_path, const char *overlay_apk_path,
        const char *idmap_path)
{
    //這個也沒啥好說的,又是一堆檢查。。。
    if (!verify_root_or_system()) {
        fprintf(stderr, "error: permission denied: not user root or user system\n");
        return -1;
    }
    if (!verify_file_readable(target_apk_path)) {
        ALOGD("error: failed to read apk %s: %s\n", target_apk_path, strerror(errno));
        return -1;
    }
    if (!verify_file_readable(overlay_apk_path)) {
        ALOGD("error: failed to read apk %s: %s\n", overlay_apk_path, strerror(errno));
        return -1;
    }
    return idmap_create_path(target_apk_path, overlay_apk_path, idmap_path);
}

//frameworks/base/cmds/idmap/create.cpp
int idmap_create_path(const char *target_apk_path, const char *overlay_apk_path,
        const char *idmap_path)
{
    //已經是新的沒必要做了
    if (!is_idmap_stale_path(target_apk_path, overlay_apk_path, idmap_path)) {
        // already up to date -- nothing to do
        return EXIT_SUCCESS;
    }
    //打開檔案
    int fd = open_idmap(idmap_path);
    if (fd == -1) {
        return EXIT_FAILURE;
    }
    //做idmap,并寫入檔案
    int r = create_and_write_idmap(target_apk_path, overlay_apk_path, fd, false);
    close(fd);
    if (r != 0) {
        unlink(idmap_path);
    }
    return r == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}

   int create_and_write_idmap(const char *target_apk_path, const char *overlay_apk_path,
            int fd, bool check_if_stale)
   {
        //又是檢查。。。
        if (check_if_stale) {
            if (!is_idmap_stale_fd(target_apk_path, overlay_apk_path, fd)) {
                // already up to date -- nothing to do
                return 0;
            }
        }
        uint32_t *data = NULL;
        size_t size;
 
        //在這裡做idmap
        if (create_idmap(target_apk_path, overlay_apk_path, &data, &size) == -1) {
            return -1;
        }
        //标準的寫檔案操作,就不說了
        if (write_idmap(fd, data, size) == -1) {
            free(data);
            return -1;
        }
        free(data);
        return 0;
   }
   int create_idmap(const char *target_apk_path, const char *overlay_apk_path,
        uint32_t **data, size_t *size)
   {
        //CRC校驗
        uint32_t target_crc, overlay_crc;
        if (get_zip_entry_crc(target_apk_path, AssetManager::RESOURCES_FILENAME,
				&target_crc) == -1) {
            return -1;
        }
        if (get_zip_entry_crc(overlay_apk_path, AssetManager::RESOURCES_FILENAME,
				&overlay_crc) == -1) {
            return -1;
        }
        //繞了半天,這個活兒還是AssetManager來幹的^_^
        AssetManager am;
        bool b = am.createIdmap(target_apk_path, overlay_apk_path, target_crc, overlay_crc,
                data, size);
        return b ? 0 : -1;
   }
           

        其實idmap程序還是圍觀者,隻不過是做權限、通路、CRC等校驗,真正的工作是由AssetManager來完成的。帶着idmap檔案那奇怪的資料形式,我們将在下期介紹idmap檔案具體是怎麼生成和加載的。