一、替換應(yīng)用資源
1. 實現(xiàn)主題包apk中的資源替換原來apk


- 主題包需要完成工作
(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 主要完成了已下工作:
-
根據(jù)主題包apk和原apk路徑創(chuàng)建idmap文件
data/resource-cache/
data@com.example.mcdulltheme@base.apk@idmap
com.example.mcdulltheme是主題包apk包名
將主題包路徑添加到原應(yīng)用資源路徑resourceDirs中
- 資源加載過程

上圖為應(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資源


上圖中第二個圖片是安裝了我自己制作的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);