Open Theme

一、替換應(yīng)用資源

1. 實現(xiàn)主題包apk中的資源替換原來apk

old.jpg

new.jpg
  • 主題包需要完成工作

(1). AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
    android:versionCode="664" 
    android:versionName="3.00.13" 
    package="com.example.mcdulltheme" 
    platformBuildVersionCode="21" 
    platformBuildVersionName="5.0-1521886">
    <application android:allowBackup="false" android:hasCode="false"/>
    <overlay android:priority="1" android:targetPackage="com.example.oldtheme"/>
</manifest>

(2). 覆蓋OldTheme.apk 的資源

 可以替換圖片、顏色、字符串等,但不可以替換xml資源,比如布局文件。

(3). 殺OldTheme.apk進(jìn)程重新啟動,資源替換成功。

2. framework層實現(xiàn)原理

  • 主題包安裝過程

    apk安裝過程中會調(diào)用PackageManagerService.createIdmapForPackagePairLI,這個方法在資源替換過程中起著重要作用。

/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

    //參數(shù):pkg 要覆蓋的apk, 從AndroidManifest.xml 里面的targetPackage讀到的
    //opkg 主題資源apk
    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;
        }
        //mOverlays 以目標(biāo)apk為鍵值,對應(yīng)的所有主題包apk信息
        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);
        // TODO: generate idmap for split APKs
        try {
            //在Install守護(hù)進(jìn)程創(chuàng)建Idmap,用于查找主題包資源
            mInstaller.idmap(pkg.baseCodePath, opkg.baseCodePath, sharedGid);
        } catch (InstallerException e) {
            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);

        pkg.applicationInfo.resourceDirs = new String[overlayArray.length];
        int i = 0;
        for (PackageParser.Package p : overlayArray) {
            //將應(yīng)用資源路徑resourceDirs改為主題包路徑
            pkg.applicationInfo.resourceDirs[i++] = p.baseCodePath;
        }
        return true;
    }

createIdmapForPackagePairLI 主要完成了已下工作:

  1. 根據(jù)主題包apk和原apk路徑創(chuàng)建idmap文件

    data/resource-cache/

    data@com.example.mcdulltheme@base.apk@idmap

    com.example.mcdulltheme是主題包apk包名

  2. 將主題包路徑添加到原應(yīng)用資源路徑resourceDirs中

  • 資源加載過程
SequenceDiagram1.png

上圖為應(yīng)用啟動創(chuàng)建Resources 、 AssetManager 以及資源加載過程

我們通常會在Activity里面會使用getResources().getString(R.string.AnyString)來得到字符串或者其他資源。
getResources()最后是調(diào)用ContextImpl.getResources,返回一個Resources對象,那么我們就從Resources對象創(chuàng)建開始看。

getResources返回mResources對象,mResources對象在ContextImpl構(gòu)造函數(shù)中創(chuàng)建。
    private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
        ... ...
        Resources resources = packageInfo.getResources(mainThread);
        ... ...
        mResources = resources;
        ... ...
    }

/frameworks/base/core/java/android/app/LoadedApk.java

    public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                    mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
        }
        return mResources;
    }

/frameworks/base/core/java/android/app/ActivityThread.java

    Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
            String[] libDirs, int displayId, LoadedApk pkgInfo) {
        return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs,
                displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader());
    }

ResourcesManager.getResources會調(diào)用getOrCreateResources

    private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        ... ...

        // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
        ResourcesImpl resourcesImpl = createResourcesImpl(key);
        if (resourcesImpl == null) {
            return null;
        }

        synchronized (this) {
            ... ...

            final Resources resources;
            //這里創(chuàng)建Resources對象
            if (activityToken != null) {
                resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                        resourcesImpl);
            } else {
                resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
            }
            return resources;
        }
    }
    private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);
        //創(chuàng)建AssetManager對象
        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }

        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        //Resources的實現(xiàn)類ResourcesImpl
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
        return impl;
    }
    protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        AssetManager assets = new AssetManager();

        // resDir是apk路徑,'android'包的resDir是null,因為AssetManager會自動加載framework資源
        if (key.mResDir != null) {
            if (assets.addAssetPath(key.mResDir) == 0) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
        }

        if (key.mSplitResDirs != null) {
            for (final String splitResDir : key.mSplitResDirs) {
                if (assets.addAssetPath(splitResDir) == 0) {
                    Log.e(TAG, "failed to add split asset path " + splitResDir);
                    return null;
                }
            }
        }
        //添加overlay資源,前面主題包安裝過程中,有添加applicationInfo.resourceDirs,這里就會將主題包的資源加載到AssetManager中
        if (key.mOverlayDirs != null) {
            for (final String idmapPath : key.mOverlayDirs) {
                assets.addOverlayPath(idmapPath);
            }
        }
        //添加lib庫
        if (key.mLibDirs != null) {
            for (final String libDir : key.mLibDirs) {
                if (libDir.endsWith(".apk")) {
                    // Avoid opening files we know do not have resources,
                    // like code-only .jar files.
                    if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");
                    }
                }
            }
        }
        return assets;
    }

createAssetManager 方法中主要完成了以下工作:

1.創(chuàng)建AssetManager對象
2.調(diào)用 AssetManager.addAssetPath添加應(yīng)用程序路徑到資源管理框架
3.調(diào)用 AssetManager.addOverlayPath添加主題包資源

回到createResourcesImpl方法,得到AssetManager對象assets后,用assets為參數(shù)創(chuàng)建ResourcesImpl對象,再以這個ResourcesImpl為參數(shù)創(chuàng)建Resources對象,返回給ContextImpl。

從以上流程可以看到,應(yīng)用資源在應(yīng)用啟動時,ActivityThread.getTopLevelResources過程中將應(yīng)用資源以及相應(yīng)主題包資源加載完成,那么framework的資源什么時候加載的呢?繼續(xù)看下AssetManager的構(gòu)造函數(shù)。

/frameworks/base/core/java/android/content/res/AssetManager.java

   public AssetManager() {
       synchronized (this) {
           if (DEBUG_REFS) {
               mNumRefs = 0;
               incRefsLocked(this.hashCode());
           }
           init(false);
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }

    private static void ensureSystemAssets() {
        synchronized (sSync) {
            //sSystem 用來訪問系統(tǒng)資源的AssetManager對象,再Zygote進(jìn)程中創(chuàng)建
            if (sSystem == null) {
                AssetManager system = new AssetManager(true);
                system.makeStringBlocks(null);
                sSystem = system;
            }
        }
    }

init 調(diào)到C層AssetManager.cpp 的addDefaultAssets函數(shù)

/frameworks/base/libs/androidfw/AssetManager.cpp

static const char* kSystemAssets = "framework/framework-res.apk";
static const char* kResourceCache = "resource-cache";
bool AssetManager::addDefaultAssets()
{
    const char* root = getenv("ANDROID_ROOT");
    LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");

    String8 path(root);
    path.appendPath(kSystemAssets);
    //addAssetPath添加framework資源路徑
    return addAssetPath(path, NULL, false /* appAsLib */, true /* isSystemAsset */);
}
bool AssetManager::addAssetPath(
        const String8& path, int32_t* cookie, bool appAsLib, bool isSystemAsset)
{
    AutoMutex _l(mLock);

    asset_path ap;

    String8 realPath(path);
    if (kAppZipName) {
        realPath.appendPath(kAppZipName);
    }
    //講傳過來的資源apk path傳給asset_path對象ap
    ap.type = ::getFileType(realPath.string());
    if (ap.type == kFileTypeRegular) {
        ap.path = realPath;
    } else {
        ap.path = path;
        ap.type = ::getFileType(path.string());
        if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) {
            ALOGW("Asset path %s is neither a directory nor file (type=%d).",
                 path.string(), (int)ap.type);
            return false;
        }
    }

    // Skip if we have it already.
    for (size_t i=0; i<mAssetPaths.size(); i++) {
        if (mAssetPaths[i].path == ap.path) {
            if (cookie) {
                *cookie = static_cast<int32_t>(i+1);
            }
            return true;
        }
    }

    ALOGV("In %p Asset %s path: %s", this,
         ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string());

    ap.isSystemAsset = isSystemAsset;
    //將ap放到mAssetPaths 容器中,mAssetPaths 類型是:Vector<asset_path> mAssetPaths
    mAssetPaths.add(ap);

    // new paths are always added at the end
    //cookie用來存放當(dāng)前apk的asset_path 在mAssetPaths中的索引+1 ,然后作為參數(shù)返回
    if (cookie) {
        *cookie = static_cast<int32_t>(mAssetPaths.size());
    }

#ifdef __ANDROID__
    // Load overlays, if any
    asset_path oap;
    for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) {
        oap.isSystemAsset = isSystemAsset;
        mAssetPaths.add(oap);
    }
#endif
    //調(diào)用appendPathToResTable將ap添加到資源表ResTable中
    if (mResources != NULL) {
        appendPathToResTable(ap, appAsLib);
    }

    return true;
}

這里涉及AssetManager幾個總要變量:

  • mAssetPaths:存放所有資源包路徑

    定義: Vector<asset_path> mAssetPaths

  • mResources:存放所有資源包的ID和資源屬性對應(yīng)表

    定義:ResTable mResources

至此framework-res的資源加載到資源管理器中了,前面getTopLevelResources應(yīng)用調(diào)用java層addAssetPath ,最后也是通過native 層addAssetPath將應(yīng)用資源加載進(jìn)來, 然后繼續(xù)調(diào)用addOverlayPath將該應(yīng)用對應(yīng)的主題包資源加載進(jìn)來。其主要實現(xiàn)在AssetManager.cpp中的addOverlayPath。

bool AssetManager::addOverlayPath(const String8& packagePath, int32_t* cookie)
{
    const String8 idmapPath = idmapPathForPackagePath(packagePath);

    AutoMutex _l(mLock);
    //如果已經(jīng)存在直接返回cookie為當(dāng)前索引+1
    for (size_t i = 0; i < mAssetPaths.size(); ++i) {
        if (mAssetPaths[i].idmap == idmapPath) {
           *cookie = static_cast<int32_t>(i + 1);
            return true;
         }
     }

    ... ...

    asset_path oap;
    //主題包路徑
    oap.path = overlayPath;
    //主題包類型
    oap.type = ::getFileType(overlayPath.string());
    //生成的idmap路徑
    oap.idmap = idmapPath;
    //主題包信息添加至mAssetPaths
    mAssetPaths.add(oap);
    *cookie = static_cast<int32_t>(mAssetPaths.size());
    //將該asset_path添加到資源表mResources中
    if (mResources != NULL) {
        appendPathToResTable(oap);
    }

    return true;
 }

addOverlayPath 添加主題包資源跟前面addAssetPath類似

到現(xiàn)在應(yīng)用資源、framework資源、應(yīng)用對應(yīng)的主題包資源(只是targetPackage="應(yīng)用報名")都被加載到AssetManager資源管理框架中了。

二、替換framework資源

oldf.jpg

newf.jpg

上圖中第二個圖片是安裝了我自己制作的framework資源包后效果,資源包中只改了字體顏色資源。

Google 原生代碼是不支持主題包中資源替換framework-res.apk 中資源的,所以需要稍作改動。

(1).創(chuàng)建idmap 文件

前面說到安裝應(yīng)用主題包時,會調(diào)用到createIdmapForPackagePairLI 來創(chuàng)建idmap.
我們可以模仿普通應(yīng)用來創(chuàng)建framework-res.apk對應(yīng)的idmap.

 private boolean createIdmapForPackagePairLI(PackageParser.Package pkg,
            PackageParser.Package opkg) {
    ... ...
    //主題包AndroidMenifest.xml中將targetPackage設(shè)為"frameworkres",其他的也可以
    if(opkg.mOverlayTarget.equalsIgnoreCase("frameworkres")) {
        String frameworkPath = "/system/framework/framework-res.apk";
        //framework-res.apk 包名"android"
        final int sharedGid = UserHandle.getSharedAppGid(mPackages.get("android").applicationInfo.uid);
        // TODO: generate idmap for split APKs
        try {
            mInstaller.idmap(frameworkPath, opkg.baseCodePath, sharedGid);
        } catch (InstallerException e) {
            Slog.e(TAG, "Failed to generate idmap for " + pkg.baseCodePath + " and "
                    + opkg.baseCodePath);
            return false;
        }
    }
    ... ...
 }

(2).添加主題包路徑到 ApplicationInfo.resourceDirs

在ActivityThread.getTopLevelResources時或者之前,或者直接在createIdmapForPackagePairLI創(chuàng)建idmap之后添加主題包路徑到 ApplicationInfo.resourceDirs

 if(opkg.mOverlayTarget.equalsIgnoreCase("frameworkres")) {
    for (String overlayTarget : mOverlays.keySet()) {
        ArrayMap<String, PackageParser.Package> map = mOverlays.get(overlayTarget);
        if (map != null) {
            map.put(opkg.packageName,opkg);
            PackageParser.Package[] overlayArray =
            map.values().toArray(new PackageParser.Package[0]);
            if (overlayTarget.equals("frameworkres")) 
                overlayTarget = "android";
            PackageParser.Package pkg = mPackages.get(overlayTarget);
                    pkg.applicationInfo.resourceDirs = new String[overlayArray.length];
            int i = 0;
            for (PackageParser.Package p : overlayArray) {
                //將應(yīng)用資源路徑resourceDirs改為主題包路徑
                pkg.applicationInfo.resourceDirs[i++] = p.baseCodePath;
            }
        }

    }
}

(3).調(diào)用AssetManager.addOverlayPath 添加主題包信息

mContext.getResources().getAssets().addOverlayPath(opkg.baseCodePath);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容