精选文章 Webview.apk —— Google 官方的私有插件化方案

Webview.apk —— Google 官方的私有插件化方案

作者:一肥仔啊 时间: 2021-02-05 09:43:15
一肥仔啊 2021-02-05 09:43:15
【摘要】简介: 在 Android 跨入 5.0 版本之后,我们在使用 Android 手机的过程中,可能会发现一个奇特的现象,就是手机里的 WebView 是可以在应用商店升级,而不需要跟随系统. 
 
在 Android 跨入 5.0 版本之后,我们在使用 Android 手机的过程中,可能会发现一个奇特的现象,就是手机里的 WebView 是可以在应用商店升级,而不需要跟随系统的。 
这一点在...

简介: 在 Android 跨入 5.0 版本之后,我们在使用 Android 手机的过程中,可能会发现一个奇特的现象,就是手机里的 WebView 是可以在应用商店升级,而不需要跟随系统.

Webview.apk —— Google 官方的私有插件化方案1

在 Android 跨入 5.0 版本之后,我们在使用 Android 手机的过程中,可能会发现一个奇特的现象,就是手机里的 WebView 是可以在应用商店升级,而不需要跟随系统的。

这一点在 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

其中 PP 是 packageId,TT 是 typeId,EEEE 是按规律出来的实体ID(EntryId),今天我们要关注的是前四位。如果你曾经关注的话,你大概会知道,我们写出来的 App,一般 PP 值是 7F。

我们知道 android 针对不同机型以及不同场景,定义了许许多多 config,最经典的多语言场景:values/values-en/values-zh-CN 我们使用一个字符串资源可能使用的是相同的 ID,但是拿到的具体值是不同的。这个模型就是一个表模型 —— id 作为主键,查询到一行数据,再根据实际情况选择某一列,一行一列确定一个最终值:

Webview.apk —— Google 官方的私有插件化方案2

这种模型对我们在不同场景下需要使用“同一含义”的资源提供了非常大的便捷。Android 中有一个类叫 AssetManager 就是负责读取 R 中的 id 值,最终到一个叫 resources.arsc 的表中找到具体资源的路径或者值返回给 App 的。

插件化中的资源固定

我们经常听见 Android 插件化方案里,有一个概念叫 固定ID,这是什么意思呢?我们假设一开始一个 App 访问的资源 id 是 0x7f0103,它是一张图片,这时候我们下发了新的插件包,在构建的过程中,新增了一个字符串,恰好这张图片在编译中进行了某种排序,排序的结果使得 oxPPTT 中的 string 的 TT 变成了 01,于是这个字符串的 id 又恰好变成了 0x7f0103。那么老代码再去访问这个资源的时候,访问 0x7f0103,这时候拿到的不再是图片,而是一个字符串,那么 App 的 Crash 就是灾难性的了。

因此,我们期望资源 id 一旦生成,就不要再动来动去了。但是这里又有一个非常显眼的问题:如果 packageId 永远是 7f,那么显然是不够用的,我们知道有一定的方案可以更改 packgeId,只要在不同业务包中使用不同的 packageId,这样能极大避免 id 碰撞的问题,为插件化使用外部资源提供了条件。

等等!我们在开头说到了 webview.apk 的更新 —— 代码,资源都可以更新。这听上去不就是插件化的一种吗?Google 应用开发者无感知的情况下,到底是怎么实现 webview 的插件化的呢?如果我们揭开了这一层神秘的面纱,我们是不是也可以用这个插件化的特性了呢?

答案当然是肯定的。

WebView APK 和 android 系统资源

我作为一个 Android 工具链开发,在开始好奇 webview 的时候,把 webview.apk 下载过来的第一时间,就是把它拖进 Android Studio,看一看这个 APK 到底有哪里不同。

Webview.apk —— Google 官方的私有插件化方案3

仔细看,它资源的 packgeId 是 00!直觉告诉我,0 这个值很特殊。

我们再看下大名鼎鼎的 android sdk 中的 android.jar 提供的资源。

这里说个题外话,我们使用 android 系统资源,比如 @android:color/red 这样的方式,其实就是使用到了 android.jar 中提供的资源。我们可以把这个 android.jar 重命名成 android.apk,拖进 Android Studio 中进行查看。

Webview.apk —— Google 官方的私有插件化方案4

我们看到,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文件:



然后查看编译出来的结果

Webview.apk —— Google 官方的私有插件化方案5

我们看见 android:background 的值变成了 @ref/0x0106000c。这个 apk 在 Android 手机上运行的时候,会在 AssetsManager 里面加载两个资源包,一个是自己的 App 资源包,一个是 android framework 资源包,这时候去找 0x0106000c 的时候,就会找到系统的资源里面去。

有一个 android.jar 是个特殊的 01 没问题,那如果系统中存在许多的 apk,他们的值分别是 2,3,4,5,…… 想想都觉得要天下大乱了,如果这是真的,他们怎么管理这些资源 packageId 呢?

带着这些好奇,我下载了 aapt 的源码,准备在真相世界里一探究竟。

AAPT 源码,告诉你一切

下载源码过程和编译过程就不讲了,为了调试方便,建议大家编译出一个没有优化的 aapt debug 版,内涵是使用 -Oθ 关闭优化,并使用 debug 模式编译即可,我使用的版本是 android 28.0.3 版本。

我们首先可以先瞅一眼,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 包资源。

我们知道,在 xml 中引用其他资源包的方式,是使用@开头的,所以,假设你需要使用 webview 中的资源的时候,你需要指定包名,其实我们在使用 android 提供的资源的时候也是这么做的,还记得 @android:color/black 吗? 其实 @android 中的 android 就是 android.jar 里面资源的包名,我们再看一眼 android.jar 的包格式,注意图中的 packageName:

Webview.apk —— Google 官方的私有插件化方案6

知道这点以后,我们使用 webview 中的资源的方式就变成如下例子:



我们执行下编译,发现报错了:

res/layout/layoutactivity.xml:2: error: Error: Resource is not public. (at 'src'

with value '@com.google.android.webview:drawable/iconwebview').

如果你之前使用过 public.xml 这个文件的话(你可能在这见过它:https://developer.android.com/studio/projects/android-library.html#PrivateResources),那么这里我需要说明下 —— 不仅仅是 library 有 private 资源的概念,跨 apk 使用资源同样有 public 的概念。但是,这个 public 标记像 aar 一样,其实并不是严格限制的。

在使用 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;
} else if (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); }
return false;
}

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."); }
return false; } } }
// ...
}

我们查看上面相关的代码,知道只要关闭 enforcePrivate 这个开关即可,查看这一段逻辑,可以很轻松得到结论,只要这样写就行了:



注意 @ 和包名之间多了一个 *,这个星号,就是无视私有资源直接引用的意思,再一次使用 aapt 编译,资源编译成 功。查看编译出来的文件

Webview.apk —— Google 官方的私有插件化方案7

看我们的引用变成了 @dref/0x02060061 咦,packageId 怎么变成了 02,没关系,我们后面的篇章解开这个谜底。

DynamicRefTable

我们根据刚刚上面的源码往下看,继续看 stringToValue 这个函数,会看见这么一段代码

 
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;

这段代码告诉我们几件事:

  1. 刚刚的 webview 的 packageId 是经过 remapp 后的
  2. 它的类型变成了 TYPEDYNAMICREFERENCE看英文翻译是“动态引用”的意思。

我们使用 aapt d--values resourcesout.apk命令把资源信息打印出来,可以发现

Webview.apk —— Google 官方的私有插件化方案8

这里有关的是一个 DynamicRefTable,看它里面的值,好像是 packageId 和 packageName 映射。也就是说,0x02 的 packageId 所在的资源,应该是在叫com.google.android.webview 的包里的。

我们查询 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;
}

这里有关的是一个 DynamicRefTable,看它里面的值,好像是 packageId 和 packageName 映射。也就是说,0x02 的 packageId 所在的资源,应该是在叫com.google.android.webview 的包里的。

我们查询 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;
}

得到几个结论:

  1. 如果 packageId 是 0x7f 的话,不转换,原来的 ID
  2. 还是原来的 ID如果 packageId 是 0 或者 packageId 是 7f 且 mAppAsLib 是真的话,把 packgeId 换成 mAssignedPackageId
  3. 否则从 mLookupTable 这个表中做一个映射,换成 translatedId 返回。

条件一很明确,二的话应该是 webview.apk 访问自己的资源情况,暂时不管。条件三就是我们现在想要知道的场景了。

我对 mLookupTable 这个变量非常好奇,于是跟踪调用,查看定义,最终找到一些关键信息,在 AssetManager2 中找到相关代码,我们给它添加额外的注释说明

 
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& 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(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(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(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 维护的问题,因为 pacakgeId 可以重置,我们只要维护 packageName 就行了。

总结

经过以上的调研,我们目前知道了Google 官方的“插件化资源”是如何实现的。但是这个方案也有一个弊端,就是在 5.0 以下的手机上会 crash,原因是 5.0 以下的系统并不认识 TYPEDYNAMICREFERENCE 这个类型。因此如果你的 App 还需要支持 5.0 以下的应用的话,还需要经过一些修改才能实现:

  1. 依然需要手动管理 packageId。
  2. 把 aapt 中关于 dynamic reference 的地方改成 reference。

期待各大厂商在努力更新 Android 版本上能迈出更大的步伐,一旦 5.0 以下的手机绝迹,我相信我们的 Android App 生态也会变得更加美好。

 

END

Webview.apk —— Google 官方的私有插件化方案9

 

Webview.apk —— Google 官方的私有插件化方案10

Webview.apk —— Google 官方的私有插件化方案11

勿删,copyright占位
分享文章到微博
分享文章到朋友圈

上一篇:免签支付接口和三方支付接口有什么不同?

下一篇:SpringBoot 使用过滤器、拦截器、切面(AOP),及其之间的区别和执行顺序

您可能感兴趣

  • 使用COM+参数化对象结构编程技术

    使用COM+参数化对象结构编程技术 一、引言COM+ 服務技術的一個核心理念就是系統本身提供完成許多面向企業應用開發者的通用基本模組(如線程、物件資源池管理、事務服務管理、事件服務管理等),讓系統設計人員把精力集中在企業應用本身的業務邏輯上。COM+提供了許多新的服務和一致的管理運行環境,它還支援申明性編程模型(declarative programming model),也就是說,開發人員...

  • Delphi 插件(Plug-ins)创建、调试与使用应用程序扩展(二)

    延伸父应用 这个简单的插件不错,不过它不能做什么有用的事情。第二个例子就是纠正这个问题。这个插件的目标就是在父应用程序的主菜单中加入一个项目。这个菜单项目,当被单击时,就会执行插件内的一些代码。图6显示外壳程序的改进版,两个插件都已经加载。在这个版本的外壳程序中,一个名为Plug-in的新菜单项目,被添加到主菜单中。插件会在运行时加入一个菜单项。 图6:加载了两个插件的外壳程序的改进版   ...

  • Atl实战系列《用ATL COM Wizard 实现(VC++的插件)Add-in》

    要实现Add-in Object 有三种方法,一种是利用ATL COM Wizard中的ATL对象 Add-in Object来实现,另一种是利用DevStudio Add-in Wizard来实现,第三种就是手工写所有的代码,如果手工写,一定要记住实现IDSAddIn接口,这样才能才能增加新的命令和工具条到VC的IDE环境中去。  我主要是讲解ATL COM Wizard,所以我用第一种方...

  • 使用xml+jsp技术实现网页内容动态显示的方案

    一、xml技术简介 XML(eXtensible Markup Language——可扩展标记语言)是一种 扩展的源标记语言,是可以定义其他语言的语言。它是SGML的一个简化子集,这个子集是专为Web环境设计的,这就是说,它必须处理面向数据处理的(而不是面向发布的)、生存期短的(实际上是动态产生的)信息。 XML与HTML对比: l         XML语法严格,数据与显示分离,便于其他程...

  • 离开Google的日子

    星期天躲在家里看了两天刘醒龙的小说,远离了各种媒体上乱叨叨的小道消息和在服务器上半躺着的文档与代码,称得上是“偷得浮生半日闲”,刘醒龙极擅编故事,又喜欢将侠骨柔情煽地热火朝天,所以他的小说看起来很过瘾,用我大哥的话来讲就是“处处充满了浪漫的英雄主义色彩”,看完之后你就会又开始相信中国还是有男人的。     周一带着这种信心又坐在电脑前,开工之前先瞄了一眼体育新闻,见到吵吵了一个多月的罗纳尔多...

  • 编程语言的层次观点——兼谈C++的剪裁方案

    几个月前,我在CSDN上发表了一篇短文,叫《风格的选择》。在那篇文章里,我提出一个观点:面对不同应用领域和环境,C++的开发者应该主动剪裁语言,选择最合适自己领域的C++子集进行具体开发。这是从我的实际工作中总结出来的想法,始终只是一种经验主义的东西。我觉得这是正确的选择,但是如果有人问我为什么,我自己也深感困惑,无法回答。最近一段时间,因为工作方向的调整,我逐渐偏向低层技术,对于计算机体系...

  • 校园网接入Internet方案一例

    校园网接入Internet方案一例 李潘龙   教育部在“校校通”工程的总体部署中提出,利用5~10年的时间使全国90%左右独立建制的中小学校能够上网,使中小学师生能共享网上教育资源,全面提高所有中小学的教育教学质量。因此,选择一个合适的校园网接入Internet方案,需要校园网建设决策者仔细考虑。     可选的接入方式   目前校园网接入Internet的方式主要有:光纤、DDN、IS...

  • 对象化JS之----日期选择

CSDN

CSDN

中国开发者社区CSDN (Chinese Software Developer Network) 创立于1999年,致力为中国开发者提供知识传播、在线学习、职业发展等全生命周期服务。

华为云40多款云服务产品0元试用活动

免费套餐,马上领取!
Webview.apk —— Google 官方的私有插件化方案介绍:华为云为您免费提供Webview.apk —— Google 官方的私有插件化方案在博客、论坛、帮助中心等栏目的相关文章,同时还可以通过 站内搜索 查询更多Webview.apk —— Google 官方的私有插件化方案的相关内容。| 移动地址: Webview.apk —— Google 官方的私有插件化方案 | 写博客