AndroidQ RRO(Runtime Resource Overlay)机制(4)
作者:互联网
概述
上篇文章说到目标应用的Overlay包路径被更新到了目标应用ApplicationInfo
之后,就会将更新之后的ApplicationInfo
传给APP进程,本篇继续来看APP进程的处理。
ApplicationThread.scheduleApplicationInfoChanged
public void scheduleApplicationInfoChanged(ApplicationInfo ai) {
mH.removeMessages(H.APPLICATION_INFO_CHANGED, ai);
sendMessage(H.APPLICATION_INFO_CHANGED, ai);
}
ActivityThread.handleApplicationInfoChanged
@VisibleForTesting(visibility = PACKAGE)
public void handleApplicationInfoChanged(@NonNull final ApplicationInfo ai) {
//LoadedApk用来描述APP进程中一个package的详细信息
LoadedApk apk;
LoadedApk resApk;
//一个APP的LoadedApk通常是放在mPackages中,mResourcePackages通常存放为其他资源包应用创建的LoadedApk
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref = mPackages.get(ai.packageName);
apk = ref != null ? ref.get() : null;
ref = mResourcePackages.get(ai.packageName);
resApk = ref != null ? ref.get() : null;
}
final String[] oldResDirs = new String[2];
if (apk != null) {
oldResDirs[0] = apk.getResDir();
final ArrayList<String> oldPaths = new ArrayList<>();
LoadedApk.makePaths(this, apk.getApplicationInfo(), oldPaths);
//将AMS传过来的,已经更新了的ApplicationInfo再更新到APP进程的LoadedApk
apk.updateApplicationInfo(ai, oldPaths);
}
if (resApk != null) {
oldResDirs[1] = resApk.getResDir();
final ArrayList<String> oldPaths = new ArrayList<>();
LoadedApk.makePaths(this, resApk.getApplicationInfo(), oldPaths);
resApk.updateApplicationInfo(ai, oldPaths);
}
synchronized (mResourcesManager) {
// Update all affected Resources objects to use new ResourcesImpl
mResourcesManager.applyNewResourceDirsLocked(ai, oldResDirs);
}
ApplicationPackageManager.configurationChanged();
Configuration newConfig = new Configuration();
newConfig.assetsSeq = (mConfiguration != null ? mConfiguration.assetsSeq : 0) + 1;
//回调onConfigurationChanged
handleConfigurationChanged(newConfig, null);
// 重启Activity使得Overlay资源生效,这里的重启不是整个应用被杀掉那种重启,preserveWindows代表是否保留窗口
relaunchAllActivities(true /* preserveWindows */);
}
APP这边拿到新的ApplicationInfo
之后会将其更新到自己的LoadedApk
中,之后会回调onConfigurationChanged
方法,最后是重启所有Activity
,使Overlay资源能够生效,我们看到这里只是针对Activity
可以重启生效,但如果是对SystemUI这种只有window的应用是不行的,那SystemUI是怎么生效的呢? 依靠的就是onConfigurationChanged
的回调,其回调入口就在SystemUIApplication
中,接着重点看下ApplicationInfo
的更新过程:
LoadedApk.updateApplicationInfo
public void updateApplicationInfo(@NonNull ApplicationInfo aInfo,
@Nullable List<String> oldPaths) {
//将ApplicationInfo保存到LoadedApk,主要是更新了mOverlayDirs
//mOverlayDirs = aInfo.resourceDirs
setApplicationInfo(aInfo);
......
synchronized (this) {
......
//重建目标应用的ResourcesManager
mResources = ResourcesManager.getInstance().getResources(null, mResDir,
splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
getClassLoader());
}
}
..
}
只关注上面代码两部分,一部分是将新的ApplicationInfo
更新到LoadedApk
,第二部分就是新ApplicationInfo
中的数据然后重建ResourcesManager,对于RRO来说主要更新就是mOverlayDirs
这个变量,它代表目标应用的Overlay包的路径,详见上一篇:
private void setApplicationInfo(ApplicationInfo aInfo) {
......
mOverlayDirs = aInfo.resourceDirs;
......
}
接着我们需要看的是ResourcesManager
的重建:
ResourcesManager.getResources
public @Nullable Resources getResources(@Nullable IBinder activityToken,
@Nullable String resDir,
@Nullable String[] splitResDirs,
@Nullable String[] overlayDirs,
@Nullable String[] libDirs,
int displayId,
@Nullable Configuration overrideConfig,
@NonNull CompatibilityInfo compatInfo,
@Nullable ClassLoader classLoader) {
try {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
final ResourcesKey key = new ResourcesKey(
resDir,
splitResDirs,
overlayDirs,
libDirs,
displayId,
overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
compatInfo);
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
return getOrCreateResources(activityToken, key, classLoader);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
上面的变量很多,我们关心的就两个,resDir
代表目标应用的安装路径,overlayDirs
代表Overlay资源包的安装路径,可以有多个Overlay,ResourcesKey
用来保存这些信息。
ResourcesManager.getOrCreateResources
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
//activityToken为空
if (activityToken != null) {
....
} else {
// 从缓存中去拿ResourcesImpl,这里面有条件的,并不是说缓存中有就一定会取,还会判断当前的ResourcesImpl
//是不是最新的,对于RRO来说不会是最新的,所以resourcesImpl为空
ResourcesImpl resourcesImpl = findResourcesImplF`在这里插入代码片`orKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
}
// 创建新的ResourcesImpl
ResourcesImpl resourcesImpl = createResourcesImpl(key);
if (resourcesImpl == null) {
return null;
}
// 缓存下来
mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
final Resources resources;
if (activityToken != null) {
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
} else {
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
return resources;
}
}
因为加载了Overlay包之后即使缓存中有ResourcesImpl
,但因为不是最新的,也是需要重新创建的:
ResourcesManager.createResourcesImpl
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
....
final AssetManager assets = createAssetManager(key);
....
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
if (DEBUG) {
Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
}
return impl;
}
这里我们只关注AssetManager
的创建:
ResourcesManager.createAssetManager
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
final AssetManager.Builder builder = new AssetManager.Builder();
//应用自身的资源包
if (key.mResDir != null) {
try {
builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,
false /*overlay*/));
} catch (IOException e) {
Log.e(TAG, "failed to add asset path " + key.mResDir);
return null;
}
}
//拆分应用的资源包
if (key.mSplitResDirs != null) {
for (final String splitResDir : key.mSplitResDirs) {
try {
builder.addApkAssets(loadApkAssets(splitResDir, false /*sharedLib*/,
false /*overlay*/));
} catch (IOException e) {
Log.e(TAG, "failed to add split asset path " + splitResDir);
return null;
}
}
}
//Overlay资源包
if (key.mOverlayDirs != null) {
for (final String idmapPath : key.mOverlayDirs) {
try {
builder.addApkAssets(loadApkAssets(idmapPath, false /*sharedLib*/,
true /*overlay*/));
} catch (IOException e) {
Log.w(TAG, "failed to add overlay path " + idmapPath);
// continue.
}
}
}
//共享资源包
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.
try {
builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/,
false /*overlay*/));
} catch (IOException e) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
// continue.
}
}
}
}
return builder.build();
}
每个应用在创建createAssetManager
时都会加载四种类型的资源包(如果有的话),第一个是应用自身,第二个是拆分的资源包(Android5.0之后,支持将一个应用拆分为多个包),第三个是Overlay资源包,第四个是共享资源包(其他应用共享给当前应用的资源,这个和Overlay有什么区别呢?主要区别就是共享资源包需要目标应用自己在代码中引入,并且明确表明需要使用,如引用某个共享资源包的资源需要加其包名前缀)。
上面四种资源包的加载流程都一样的,我们以Overlay包为例,loadApkAssets
根据资源包的路径构造ApkAssets
对象,并通过addApkAssets
添加到AssetManager
中:
ResourcesManager.loadApkAssets
private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay)
throws IOException {
final ApkKey newKey = new ApkKey(path, sharedLib, overlay);
ApkAssets apkAssets = null;
//省略缓存相关的代码
.....
// We must load this from disk.
if (overlay) {
apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path),
false /*system*/);
} else {
apkAssets = ApkAssets.loadFromPath(path, false /*system*/, sharedLib);
}
if (mLoadedApkAssets != null) {
mLoadedApkAssets.put(newKey, apkAssets);
}
mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets));
return apkAssets;
}
loadApkAssets
三个参数分别为资源包的安装路径,是否共享,是否为Overlay包,我们这里主要看的是目标应用对Overlay包的加载,所以后面都沿着Overlay流程去分析,overlayPathToIdmapPath
这个方法相当重要,它会将Overlay包的安装路径进行转换:
private static String overlayPathToIdmapPath(String path) {
return "/data/resource-cache/" + path.substring(1).replace('/', '@') + "@idmap";
}
比如我们Overlay包安装路径为"product/overlay/RROResource/RROResource.apk"
,转换之后的路径就为"/data/resource-cache/product@overlay@RROResource@RROResource.apk@idmap"
,这是个什么路径呢?前面
AndroidQ RRO(Runtime Resource Overlay)机制(2)
说过,这个路径就是Overlay包的Idmap文件的生成路径,这个文件中包含了目标应用与Overlay应用相同资源名称间的映射关系,通过命令adb shell idmap2 dump --idmap-path [file]
可以查看:
[target res id] - > [overlay res id] [resource name]
0x01040151 -> 0x01050001 string/config_dozeComponent
0x01040152 -> 0x01050002 string/config_dozeDoubleTapSensorType
0x01040153 -> 0x01050003 string/config_dozeLongPressSensorType
继续看加载:
ApkAssets.loadOverlayFromPath
public static @NonNull ApkAssets loadOverlayFromPath(@NonNull String idmapPath, boolean system)
throws IOException {
return new ApkAssets(idmapPath, system, false /*forceSharedLibrary*/, true /*overlay*/);
}
private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
throws IOException {
Preconditions.checkNotNull(path, "path");
mNativePtr = nativeLoad(path, system, forceSharedLib, overlay);
mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
}
可以发现最终的加载都在native层实现,Java层ApkAssets
会保存native层ApkAssets
的引用,参数system
表示是否为framework-res.apk,显然这里为false。
ApkAssets.nativeLoad
static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboolean system,
jboolean force_shared_lib, jboolean overlay) {
ScopedUtfChars path(env, java_path);
if (path.c_str() == nullptr) {
return 0;
}
..
std::unique_ptr<const ApkAssets> apk_assets;
//不同类型的资源包有不同的加载方式
if (overlay) {
apk_assets = ApkAssets::LoadOverlay(path.c_str(), system);
} else if (force_shared_lib) {
apk_assets = ApkAssets::LoadAsSharedLibrary(path.c_str(), system);
} else {
apk_assets = ApkAssets::Load(path.c_str(), system);
}
...
return reinterpret_cast<jlong>(apk_assets.release());
}
我们关注的是Overlay:
ApkAssets::LoadOverlay
std::unique_ptr<const ApkAssets> ApkAssets::LoadOverlay(const std::string& idmap_path,
bool system) {
std::unique_ptr<Asset> idmap_asset = CreateAssetFromFile(idmap_path);
if (idmap_asset == nullptr) {
return {};
}
const StringPiece idmap_data(
reinterpret_cast<const char*>(idmap_asset->getBuffer(true /*wordAligned*/)),
static_cast<size_t>(idmap_asset->getLength()));
std::unique_ptr<const LoadedIdmap> loaded_idmap = LoadedIdmap::Load(idmap_data);
if (loaded_idmap == nullptr) {
LOG(ERROR) << "failed to load IDMAP " << idmap_path;
return {};
}
return LoadImpl({} /*fd*/, loaded_idmap->OverlayApkPath(), std::move(idmap_asset),
std::move(loaded_idmap), system, false /*load_as_shared_library*/);
}
这个方法里面全是对文件流的操作,CreateAssetFromFile
会打开idmap_path,构造Asset
,idmap_data
代表从Idmap文件读取到的数据,这些数据又被构造成LoadedIdmap
对象,loaded_idmap->OverlayApkPath
返回的是Overlay包的安装路径。
ApkAssets::LoadImpl
std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl(
unique_fd fd, const std::string& path, std::unique_ptr<Asset> idmap_asset,
std::unique_ptr<const LoadedIdmap> loaded_idmap, bool system, bool load_as_shared_library) {
::ZipArchiveHandle unmanaged_handle;
int32_t result;
//打开Overlay apk压缩包
if (fd >= 0) {
result =
::OpenArchiveFd(fd.release(), path.c_str(), &unmanaged_handle, true /*assume_ownership*/);
} else {
result = ::OpenArchive(path.c_str(), &unmanaged_handle);
}
.....
//构造native层ApkAssets
std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(unmanaged_handle, path, last_mod_time));
// kResourcesArsc = "resources.arsc"
::ZipString entry_name(kResourcesArsc.c_str());
::ZipEntry entry;
//找到"resources.arsc"
result = ::FindEntry(loaded_apk->zip_handle_.get(), entry_name, &entry);
.....
// 打开"resources.arsc"
loaded_apk->resources_asset_ = loaded_apk->Open(kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER);
...
loaded_apk->idmap_asset_ = std::move(idmap_asset);
const StringPiece data(
reinterpret_cast<const char*>(loaded_apk->resources_asset_->getBuffer(true /*wordAligned*/)),
loaded_apk->resources_asset_->getLength());
//加载"resources.arsc"的数据
loaded_apk->loaded_arsc_ =
LoadedArsc::Load(data, loaded_idmap.get(), system, load_as_shared_library);
.....
return std::move(loaded_apk);
}
LoadImpl
会通过OpenArchiveFd
解压缩Overlay包(压缩算法为zip),接着构造native层ApkAssets
对象,然后通过FindEntry
找到Overlay包的resources.arsc
,最后打开resources.arsc
并读取其中的数据。
resources.arsc
是一个二进制文件,是由Android的aapt打包工具生成的,它里面包含了当前应用的资源数据,其结构非常复杂,要理解resources.arsc
首先必须熟悉其中的数据结构,关于resources.arsc
解析不在本篇讨论范围中。
resources.arsc
的加载由LoadedArsc
来完成,我们来简单看看它是如何实现的:
LoadedArsc::Load
std::unique_ptr<const LoadedArsc> LoadedArsc::Load(const StringPiece& data,
const LoadedIdmap* loaded_idmap, bool system,
bool load_as_shared_library) {
std::unique_ptr<LoadedArsc> loaded_arsc(new LoadedArsc());
loaded_arsc->system_ = system;
ChunkIterator iter(data.data(), data.size());
while (iter.HasNext()) {
const Chunk chunk = iter.Next();
//根据Chunk类型解析
switch (chunk.type()) {
case RES_TABLE_TYPE:
if (!loaded_arsc->LoadTable(chunk, loaded_idmap, load_as_shared_library)) {
return {};
}
break;
default:
LOG(WARNING) << StringPrintf("Unknown chunk type '%02x'.", chunk.type());
break;
}
}
....
return std::move(loaded_arsc);
}
一个resources.arsc
文件就是类型为RES_TABLE_TYPE
的Chunk,所以核心代码在LoadTable
中:
LoadedArsc::LoadTable
bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap,
bool load_as_shared_library) {
....
ChunkIterator iter(chunk.data_ptr(), chunk.data_size());
while (iter.HasNext()) {
const Chunk child_chunk = iter.Next();
switch (child_chunk.type()) {
case RES_STRING_POOL_TYPE:
if (global_string_pool_.getError() == NO_INIT) {
status_t err = global_string_pool_.setTo(child_chunk.header<ResStringPool_header>(),
child_chunk.size());
}
....
break;
case RES_TABLE_PACKAGE_TYPE: {
....
std::unique_ptr<const LoadedPackage> loaded_package =
LoadedPackage::Load(child_chunk, loaded_idmap, system_, load_as_shared_library);
if (!loaded_package) {
return false;
}
packages_.push_back(std::move(loaded_package));
} break;
default:
LOG(WARNING) << StringPrintf("Unknown chunk type '%02x'.", chunk.type());
break;
}
}
....
return true;
}
resources.arsc
文件中存放数据的Chunk大的类型分为两种,类型为RES_STRING_POOL_TYPE
的全局字符串池,类型为RES_TABLE_PACKAGE_TYPE
的包相关信息,RES_TABLE_PACKAGE_TYPE
里面还有子Chunk,所以RES_TABLE_PACKAGE_TYPE
还需要通过LoadedPackage
进一步解析,LoadedPackage
的Load
函数同样又对Chunk分类型解析,它的内部的Chunk类型比较多,解析更为复杂,由于resources.arsc
解析很复杂且不是本篇重点,感兴趣的可以自己去看看代码。
到此我们就知道了Overlay资源包的加载其核心就是解析apk的resources.arsc
文件,将其中的资源数据加载进内存,方法目标应用获取。不仅是Overlay包的加载是这样,其他资源包的加载也是同样的流程。
对APP层的处理我们主要关注的就是这部分重建AssetManager
的逻辑,接着回到ActivityThread
的handleApplicationInfoChanged
方法中,资源包加载之后为了让Overlay的资源能够生效就会relaunchAllActivities
,这里的重启Activity为了给用户好的体验会保留窗口的重启,重启之后应用加载资源时,Android资源管理框架就会去Overlay包中加载那些已经被Overlay的资源到达换肤目的。
最后附上一张流程图:
标签:RRO,Resource,idmap,Overlay,apk,loaded,path,null 来源: https://blog.csdn.net/qq_34211365/article/details/119887978