Webview.apk —— Google 官方的私有插件化方案
这一点在 iOS 中尚未实现,(iOS OTA 的历史也不是特别的悠久)。但是 webview.apk 不是一个普普通通的 apk,首先它没有图标,不算是点击启动的“App”。同时,更新这个 APK,会让所有使用 webview 的应用都得到更新,哪怕是 webview 中的 UI ,比如前进后退也一样,得到更新。
这一点是如何做到的呢?今天我们来分析下 webview 这个奇特的 APK。
Android 资源和资源ID
如果开发过 Android 的小伙伴,对 R 这个类是熟悉得不能再熟悉了,一个 R 类,里面所有的“字符串”我们都看得懂,但是一堆十六进制的数字,我们可能并不是非常的熟悉,比如看见一个 R 长这样:
public class R {public static class layout {
public static final int activity_main = 0x7f020000
}
}
后面那串十六进制的数字,我们一般称之为资源 ID (resId),如果你对 R 更熟悉一点,更可以知道资源 id 其实是有规律的,它的规律大概是
0xPPTTEEEE
我们知道 android 针对不同机型以及不同场景,定义了许许多多 config,最经典的多语言场景:values/values-en/values-zh-CN 我们使用一个字符串资源可能使用的是相同的 ID,但是拿到的具体值是不同的。这个模型就是一个表模型 —— id 作为主键,查询到一行数据,再根据实际情况选择某一列,一行一列确定一个最终值:
这种模型对我们在不同场景下需要使用“同一含义”的资源提供了非常大的便捷。Android 中有一个类叫 AssetManager 就是负责读取 R 中的 id 值,最终到一个叫 resources.arsc 的表中找到具体资源的路径或者值返回给 App 的。
插件化中的资源固定
因此,我们期望资源 id 一旦生成,就不要再动来动去了。但是这里又有一个非常显眼的问题:如果 packageId 永远是 7f,那么显然是不够用的,我们知道有一定的方案可以更改 packgeId,只要在不同业务包中使用不同的 packageId,这样能极大避免 id 碰撞的问题,为插件化使用外部资源提供了条件。
答案当然是肯定的。
WebView APK 和 android 系统资源
我们再看下大名鼎鼎的 android sdk 中的 android.jar 提供的资源。
我们看到,android.jar 中资源的 packageId 是 01。直觉告诉我,1 这个值也很特殊,(2 看上去就不那么特殊了)这个 01 的实现,其实靠猜也知道是怎么做的 —— 把 packageId 01 作为保留 id,android 系统中资源的 id 永久固定,那么所有 app 拿到的 0x01 开头的资源永远是确定的,比如,我们去查看 color/black 这个资源,查看上面那张表里的结果是 0x0106000c,那么我至少确定我这个版本所有 android 手机的 @android:color/black 这个资源的 id 全都是 0x0106000c。我们可以做一个 demo 为证,我编译一个xml文件:
<?xml version="1.0" encoding="utf-8"?><ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
</ImageView>
然后查看编译出来的结果
我们看见 android:background 的值变成了 @ref/0x0106000c。这个 apk 在 Android 手机上运行的时候,会在 AssetsManager 里面加载两个资源包,一个是自己的 App 资源包,一个是 android framework 资源包,这时候去找 0x0106000c 的时候,就会找到系统的资源里面去。
带着这些好奇,我下载了 aapt 的源码,准备在真相世界里一探究竟。
AAPT 源码,告诉你一切
我们首先可以先瞅一眼,R 下面值的定义为什么是 0xPPTTEEEE,这个定义在 ResourceType.h,同时我们发现了以下几行代码
#define Res_GETPACKAGE(id) ((id>>24)-1)#define Res_GETTYPE(id) (((id>>16)&0xFF)-1)
#define Res_GETENTRY(id) (id&0xFFFF)
#define APP_PACKAGE_ID 0x7f
#define SYS_PACKAGE_ID 0x01
前三行是 id 的定义,后两行是特殊 packageId 实锤。好了,01 被认定是系统包资源,7f 被认定为 App 包资源。
知道这点以后,我们使用 webview 中的资源的方式就变成如下例子:
<?xml version="1.0" encoding="utf-8"?><ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@com.google.android.webview:drawable/icon_webview">
</ImageView>
res/layout/layoutactivity.xml:2: error: Error: Resource is not public. (at 'src'with value '@com.google.android.webview:drawable/iconwebview').
在使用 aar 私有资源的时候,我们只要能拼出全部名称,是可以强行使用的。同时,apk,其实也有办法强行引用到这个资源,这一点我也是通过查看源码的方式得到结论的,具体在 ResourceTypes.cpp 中,有相关的代码:
bool createIfNotFound = false;const char16_t* resourceRefName;
int resourceNameLen;
if (len > 2 && s[1] == '+') {
createIfNotFound = true;
resourceRefName = s + 2;
resourceNameLen = len - 2;
} elseif (len > 2 && s[1] == '*') {
enforcePrivate = false;
resourceRefName = s + 2;
resourceNameLen = len - 2;
} else {
createIfNotFound = false;
resourceRefName = s + 1;
resourceNameLen = len - 1;
}
String16 package, type, name;
if (!expandResourceRef(resourceRefName,resourceNameLen, &package, &type, &name,
defType, defPackage, &errorMsg)) {
if (accessor != NULL) {
accessor->reportError(accessorCookie, errorMsg);
}
returnfalse;
}
uint32_t specFlags = 0;
uint32_t rid = identifierForName(name.string(), name.size(), type.string(),
type.size(), package.string(), package.size(), &specFlags);
if (rid != 0) {
if (enforcePrivate) {
if (accessor == NULL || accessor->getAssetsPackage() != package) {
if ((specFlags&ResTable_typeSpec::SPEC_PUBLIC) == 0) {
if (accessor != NULL) {
accessor->reportError(accessorCookie, "Resource is not public.");
}
returnfalse;
}
}
}
// ...
}
<?xml version="1.0" encoding="utf-8"?><ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@*com.google.android.webview:drawable/icon_webview">
</ImageView>
注意 @ 和包名之间多了一个 *,这个星号,就是无视私有资源直接引用的意思,再一次使用 aapt 编译,资源编译成
功。查看编译出来的文件
DynamicRefTable
if (accessor) {
rid = Res_MAKEID(
accessor->getRemappedPackage(Res_GETPACKAGE(rid)),
Res_GETTYPE(rid), Res_GETENTRY(rid));
if (kDebugTableNoisy) {
ALOGI("Incl %s:%s/%s: 0x%08x\n",
String8(package).string(), String8(type).string(),
String8(name).string(), rid);
}
}
uint32_t packageId = Res_GETPACKAGE(rid) + 1;
if (packageId != APP_PACKAGE_ID && packageId != SYS_PACKAGE_ID) {
outValue->dataType = Res_value::TYPE_DYNAMIC_REFERENCE;
}
outValue->data = rid;
这段代码告诉我们几件事:
刚刚的 webview 的 packageId 是经过 remapp 后的
它的类型变成了 TYPEDYNAMICREFERENCE
aapt d--values resourcesout.apk
命令把资源信息打印出来,可以发现我们查询 TYPEDYNAMICREFERENCE 和 DynamicRefTable 有关的代码,找到了这么一个函数,我们看下定义:
status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const {uint32_t res = *resId;
size_t packageId = Res_GETPACKAGE(res) + 1;
if (packageId == APP_PACKAGE_ID && !mAppAsLib) {
// No lookup needs to be done, app package IDs are absolute.
return NO_ERROR;
}
if (packageId == 0 || (packageId == APP_PACKAGE_ID && mAppAsLib)) {
// The package ID is 0x00. That means that a shared library is accessing
// its own local resource.
// Or if app resource is loaded as shared library, the resource which has
// app package Id is local resources.
// so we fix up those resources with the calling package ID.
*resId = (0xFFFFFF & (*resId)) | (((uint32_t) mAssignedPackageId) << 24);
return NO_ERROR;
}
// Do a proper lookup.
uint8_t translatedId = mLookupTable[packageId];
if (translatedId == 0) {
ALOGW("DynamicRefTable(0x%02x): No mapping for build-time package ID 0x%02x.",
(uint8_t)mAssignedPackageId, (uint8_t)packageId);
for (size_t i = 0; i < 256; i++) {
if (mLookupTable[i] != 0) {
ALOGW("e[0x%02x] -> 0x%02x", (uint8_t)i, mLookupTable[i]);
}
}
return UNKNOWN_ERROR;
}
*resId = (res & 0x00ffffff) | (((uint32_t) translatedId) << 24);
return NO_ERROR;
}
得到几个结论:
如果 packageId 是 0x7f 的话,不转换,原来的 ID 还是原来的 ID
如果 packageId 是 0 或者 packageId 是 7f 且 mAppAsLib 是真的话,把 packgeId 换成 mAssignedPackageId
否则从 mLookupTable 这个表中做一个映射,换成 translatedId 返回。
void AssetManager2::BuildDynamicRefTable() {
package_groups_.clear();
package_ids_.fill(0xff);
// 0x01 is reserved for the android package.
int next_package_id = 0x02;
const size_t apk_assets_count = apk_assets_.size();
for (size_t i = 0; i < apk_assets_count; i++) {
const ApkAssets* apk_asset = apk_assets_[i];
for (const std::unique_ptr<const LoadedPackage>& package :
apk_asset->GetLoadedArsc()->GetPackages()) {
// Get the package ID or assign one if a shared library.
int package_id;
if (package->IsDynamic()) {
//在 LoadedArsc 中,发现如果 packageId == 0,就被定义为 DynamicPackage
package_id = next_package_id++;
} else {
//否则使用自己定义的 packageId (非0)
package_id = package->GetPackageId();
}
// Add the mapping for package ID to index if not present.
uint8_t idx = package_ids_[package_id];
if (idx == 0xff) {
// 把这个 packageId 记录下来,并赋值进内存中和 package 绑定起来
package_ids_[package_id] = idx = static_cast<uint8_t>(package_groups_.size());
package_groups_.push_back({});
package_groups_.back().dynamic_ref_table.mAssignedPackageId = package_id;
}
PackageGroup* package_group = &package_groups_[idx];
// Add the package and to the set of packages with the same ID.
package_group->packages_.push_back(package.get());
package_group->cookies_.push_back(static_cast<ApkAssetsCookie>(i));
// 同时更改 DynamicRefTable 中 包名 和 packageId 的对应关系
// Add the package name -> build time ID mappings.
for (const DynamicPackageEntry& entry : package->GetDynamicPackageMap()) {
String16 package_name(entry.package_name.c_str(), entry.package_name.size());
package_group->dynamic_ref_table.mEntries.replaceValueFor(
package_name, static_cast<uint8_t>(entry.package_id));
}
}
}
// 使用 O(n^2) 的方式,把已经缓存的所有 DynamicRefTable 中的 包名 -> id 的关系全部重映射一遍
// Now assign the runtime IDs so that we have a build-time to runtime ID map.
const auto package_groups_end = package_groups_.end();
for (auto iter = package_groups_.begin(); iter != package_groups_end; ++iter) {
const std::string& package_name = iter->packages_[0]->GetPackageName();
for (auto iter2 = package_groups_.begin(); iter2 != package_groups_end; ++iter2) {
iter2->dynamic_ref_table.addMapping(String16(package_name.c_str(), package_name.size()),
iter->dynamic_ref_table.mAssignedPackageId);
}
}
}
上面的中文注释是我加的,这一段逻辑其实很简单,我们经过这样的处理,完成了 buildId -> runtimeId 的映射。也就是说,WebView 的 packageId 是在运行时动态计算生成的!
总结
依然需要手动管理 packageId。
把 aapt 中关于 dynamic reference 的地方改成 reference。
- - - - - - END - - - - - -
以上是 Webview.apk —— Google 官方的私有插件化方案 的全部内容, 来源链接: utcz.com/a/33939.html