一、替换应用资源
1. 实现主题包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进程重新启动,资源替换成功。
2. framework层实现原理
-
主题包安装过程
apk安装过程中会调用PackageManagerService.createIdmapForPackagePairLI,这个方法在资源替换过程中起着重要作用。
/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
//参数: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 以目标apk为键值,对应的所有主题包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守护进程创建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) {
//将应用资源路径resourceDirs改为主题包路径
pkg.applicationInfo.resourceDirs[i++] = p.baseCodePath;
}
return true;
}
createIdmapForPackagePairLI 主要完成了已下工作:
-
根据主题包apk和原apk路径创建idmap文件
data/resource-cache/
data@com.example.mcdulltheme@base.apk@idmap
com.example.mcdulltheme是主题包apk包名
将主题包路径添加到原应用资源路径resourceDirs中
- 资源加载过程
上图为应用启动创建Resources 、 AssetManager 以及资源加载过程
我们通常会在Activity里面会使用getResources().getString(R.string.AnyString)来得到字符串或者其他资源。
getResources()最后是调用ContextImpl.getResources,返回一个Resources对象,那么我们就从Resources对象创建开始看。
getResources返回mResources对象,mResources对象在ContextImpl构造函数中创建。
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会调用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;
//这里创建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);
//创建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的实现类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.创建AssetManager对象
2.调用 AssetManager.addAssetPath添加应用程序路径到资源管理框架
3.调用 AssetManager.addOverlayPath添加主题包资源
回到createResourcesImpl方法,得到AssetManager对象assets后,用assets为参数创建ResourcesImpl对象,再以这个ResourcesImpl为参数创建Resources对象,返回给ContextImpl。
从以上流程可以看到,应用资源在应用启动时,ActivityThread.getTopLevelResources过程中将应用资源以及相应主题包资源加载完成,那么framework的资源什么时候加载的呢?继续看下AssetManager的构造函数。
/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 用来访问系统资源的AssetManager对象,再Zygote进程中创建
if (sSystem == null) {
AssetManager system = new AssetManager(true);
system.makeStringBlocks(null);
sSystem = system;
}
}
}
init 调到C层AssetManager.cpp 的addDefaultAssets函数
/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用来存放当前apk的asset_path 在mAssetPaths中的索引+1 ,然后作为参数返回
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
//调用appendPathToResTable将ap添加到资源表ResTable中
if (mResources != NULL) {
appendPathToResTable(ap, appAsLib);
}
return true;
}
这里涉及AssetManager几个总要变量:
-
mAssetPaths:存放所有资源包路径
定义: Vector<asset_path> mAssetPaths
-
mResources:存放所有资源包的ID和资源属性对应表
定义:ResTable mResources
至此framework-res的资源加载到资源管理器中了,前面getTopLevelResources应用调用java层addAssetPath ,最后也是通过native 层addAssetPath将应用资源加载进来, 然后继续调用addOverlayPath将该应用对应的主题包资源加载进来。其主要实现在AssetManager.cpp中的addOverlayPath。
bool AssetManager::addOverlayPath(const String8& packagePath, int32_t* cookie)
{
const String8 idmapPath = idmapPathForPackagePath(packagePath);
AutoMutex _l(mLock);
//如果已经存在直接返回cookie为当前索引+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类似
到现在应用资源、framework资源、应用对应的主题包资源(只是targetPackage="应用报名")都被加载到AssetManager资源管理框架中了。
二、替换framework资源
上图中第二个图片是安装了我自己制作的framework资源包后效果,资源包中只改了字体颜色资源。
Google 原生代码是不支持主题包中资源替换framework-res.apk 中资源的,所以需要稍作改动。
(1).创建idmap 文件
前面说到安装应用主题包时,会调用到createIdmapForPackagePairLI 来创建idmap.
我们可以模仿普通应用来创建framework-res.apk对应的idmap.
private boolean createIdmapForPackagePairLI(PackageParser.Package pkg,
PackageParser.Package opkg) {
... ...
//主题包AndroidMenifest.xml中将targetPackage设为"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创建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) {
//将应用资源路径resourceDirs改为主题包路径
pkg.applicationInfo.resourceDirs[i++] = p.baseCodePath;
}
}
}
}
(3).调用AssetManager.addOverlayPath 添加主题包信息
mContext.getResources().getAssets().addOverlayPath(opkg.baseCodePath);