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

在 Android 跨入 5.0 版别之后,咱们在运用 Android 手机的进程中,或许会发现一个独特的现象,便是手机里的 WebView 是能够在运用商铺晋级,而不需求跟从体系的。

这一点在 iOS 中尚未完成,(iOS OTA 的历史也不是特别的悠长)。可是 webview.apk 不是一个普普通通的 apk,首要它没有图标,不算是点击启动的“App”。一起,更新这个 APK,会让一切运用 webview 的运用都得到更新,哪怕是 web t Z Ibview 中的 UI ,比方前进撤退也相同,得到更新。

这一点是如何做到的呢?今天咱们来分析下 webviewU g . G 这个独特的 APK。

Android 资源和资源ID

假如开发过 Android 的小伙伴,对 R 这个类A ( X / H是了解得不能再了– , ? s A解了,一个 R 类,里边一切的“字符串”咱们都看得懂,可是一堆十六进制的数字,咱们或许并不是非常的了解,比方看见一个 R 长这样:

publib v  a ` v 1 U ac class R {
public static class layout {
public static final int activity_main = 0x7f020000
}
}

后面那串十六进制的数字,咱O V D们一般称之为资源 ID (resId),假如你对 R 更了解一点,更能够知道资源 id 其实是有规则的,它的规则大概是

0xPPTTEEEE
其中 PP 是 packageId,TT 是 typeId,EEEE 是按规则出来的实体ID(EntryId),j @ s S S u今天咱们要重视的是前四位。假如你曾经, 0 % Y ^重视的话,你大概会知道,咱们写出来的 App,一般 PP 值是 7F。

咱们知道 android 针对不同机型以及不同场景,界说了许许多多 config,最经典的多语言场景:values/values-en/values-zh-CN 咱们运用一个字符串资源或许运用的是相同的 ID,可是拿T x ( o到的详细值是不同的9 X f ,。这个模型便是一个表模型 —— id 作为主键,查询到一行数据a 5 jk a W D g再依据实际情况选择某一列,一行一列确认一个终究i P k ` a P V C b值:

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

这种模型对咱们在不同场景下需求运用“同一含义”的资源供给了非常大的便捷。Android 中= A ] T E 9 |有一个类叫 AssetManager 便是担任读取 R 中的 id 值,终究到一个叫 resour: H X 5 4ces.arsc 的表中找到详细资源的路径或许值回来给 App 的。

插件化中的资源固定

咱们常常听见 Android 插件化计划里,有一个概念叫 固定ID,这是什么意思呢?咱们假设一开始s 7 0 C S % * L u一个 App 拜访的资源 id 是 0x7f0103,它是一张图片,这时分咱们下发了新的插件包,在构建的进程中,新增了一个字符串,刚好p @ g 3 R & C这张图片在编译中进行了某种排序,排序的结果使得 oxPPTT 中的 string 的 TT 变成了 01,所以这个字符串的 id 又刚好变成了 0x7f0103。那么老代码再去拜访这个资源的时分,拜访 0x7f0103,这时分拿到的不再是图片,而是一个字符串,那么 App 的 Crash 便是灾难性的了。

因而,咱们期望资源 iI 2 R + g ` Q ,d 一旦Y 5 S F X [ | R生成,就不要再动来动去了。可是这儿又有一个非常显眼的问题:假如 packageId 永远是 7f,那么显然是不够用的,咱们知道有一定的计划能够更改 packgeId,H I B – ) J只需在不同事务包中运S D S a T用不同的 packageId,这样能极大防止 id 碰撞的问题,为插件化运用外部资源供给了条件。

等等!咱们在最初说到了 webview.apk 的更q U E $ u e l 2 a新 —— 代码,资源都能够更新。这听上去n u z不便是插件化的一种吗?Google 运用开发者无感知的情况下,到底是怎样完成 webview 的插件化的呢?假如咱们揭开了这一层神秘的面纱,咱们是不是也能够用这个插件化的特性了呢?

答案当然是必定的。

WebView APK 和 android 体系资源

我作为一个 Android 工具链开发,在开始猎奇 webvif # iew 的时分,把 webview.apk 下载过{ c b E ! N %来的第一时间,便是把它拖进 Android Studio,看一j + t Z # ^ % t) K = r t w P V这个 APK 到底有哪里不同。

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

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

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

这儿说个题外话,咱们运用 android 体系资源,比方 @android:color/red 这样的方法,其实便是运用到了 android.jat I J R 4 F &r 中供给的资源。咱们能够把这个 android.jar 重命名成 android O & wd.apk,拖进 Android Stuo + &dio 中进行检查。

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

咱们看到,android.jar 中资源的 packageId 是 01。直g ? v # i y }觉告知我,1 这个值也很特别,(2 看上去就不那么特别了)这个 01 的完成,其实靠猜也知道是怎样做的 —— 把 packageId 01 作为保留 id,androi] G g T 3 ? 1 Wd 体系中资源的u @ q – 0 / # V x id 永久固定,那么一切 app 拿到的 0x01 最初的资源永远是确认的,比方,咱们去检查 color/black 这个资源,检查上面那张表里的结果是 0x0106000c,那么我至少确认我这个版别一切 anc _ P @ – 7 e Gdroid 手机的a # M y 0 @android:color/black 这个资源的 id 全都是 0x0106000c。咱们能够做一个 demo 为证,我编译一个xml文件:

<?xml version="1.0" encoding="utf-8"?>
<ImageView
xmlns:android=~ 0 - F f 1 k K"http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android@ / G 9 % 3 C Z S:layout_height="match_parentn c G K n"
android:background="@android:cc Z y golor/black">H H W 1 r ~;
</ImageView>

然后检查_ D i P zn ~ M t m ! Y d ?译出来的结果

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

咱们看见 android:background 的值变成了 @ref/0x0106000c。这个 apk 在 Android 手机上运转的时分,会在 AssetsManager 里边加载两个资源包,一个是自己的 App 资源7 – 7 g S h M r q包,一个是 android framework 资源包,这时分去找 0x0106000c 的时分,就会找p O . h v l到体系的资源里边去。

有一个 android.jar 是个特别的 01 没问题,那假如体系中存在许多的 apk,他们的值分别是x 2 b 2,3,4,5,…… 想想都觉得要天下大乱了,假如这是真的,他们怎样办R L q +理这些资源 packageId 呢?

带着这些猎奇,我下载了 aapt 的源码,预备在真相世界( F X !里一探终究。

AAPT 源码,告知你一切

下载源码进程f 4 D c 5 * J b 3和编译进程就不讲了,为了调试便利,主张咱们编译出一个没有优化的 aa, c – = } ) 4pt debug 版,内在是运用 -O 关闭优化,并运用 debug 模式编译即可,我运用的版别是 android 28.0.3 版别。

咱们首要能够先瞅一眼O 5 h & c $ o,R 下面值的界说为什么是 0xPPTTEEEE,这个界说在 ResourceType.h,一起咱们发现了以下几行代码

#define Res_GETPACKAGE(id) ((id>&. Z  % ogt;24)-1)
#define Res_GETTYPE(id) (((id>P n H 1 K V;>16)&0xFF)-1)
#define Res_GETENTRY(id) (id&0xFFFF)
#define APP_PACKAGE_ID      0x7f
#define SYS_PACKAGE_ID      0x01

前三行是 id 的L 7 . | i k & l界说,后两行是特别 packagD = b `eId 实锤。好了,01 被认定是体系包资源,7f 被认定为 App 包资源。

咱们知道c f w,在v o K S = ? W xml 中引证其他资源包/ L 5的方法,是运@ R 7 r用@最初的,所以,假设你需求运用 webview 中的资源的时分,你需求指定包名,其实咱们t n P )在运用 aj r ^ndroid 供给的资源Y b L % h / v = 3的时分也是这么做的,还记得 @android:color/black 吗` 9 u 3 S i S? 其实 @a5 ( ` Rndroid 中的 android 便是 andrZ 8 Uoid.jar 里边资源的包名,咱们再看一眼 android.jar 的包格式,留意图中的 packageName:

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

知道1 w # w f | K z v这点今后,咱们运用 webview 中的资源的方法就变成如下比如:

<?xml version="1.0" encoding="utN k C Tf-8"?>
<Imm S { c | V g m CageView
xmlnK - L ^ 2 [ 1s:android="http://schemas.android.A k [com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@com.google.android.webview:drawable/icon_wK 9 t q 3 1  Rebview">
</ImaA N * T D K g hgeView>
咱们执行下编译,发现报错了:
res/layout/layoutactivity.xml:2: era I 6 = %ror: Error: Resource is not public. (at ‘src’
with value ‘@cD % E 7 . Y yom.google.android.webview:drawable/iconwebview’).
假如你之前运用过 public.xml 这个文件的话(你或许在这见过它:https://developer.android.com/studio/projects/android-library.html#PrivateResources),那么这儿我 1 / x需求阐明下 —— 不仅仅是 library 有 private 资源的概念,跨 apk 运用资V = & A源同样有 publix ? ~ L K G z X 3c 的概念。可是,这个 public 标记像 aar 相同,其实并不是严格约束的。

在运用 aa0 r C Y 2r 私有资源的时分,咱们只需能拼出悉数称号,是能够强行运用的。一起,apk,其实也有办法强行引证到这个资源,这一点我也是通过检查源码的方法得到定论的,详细在 ResourceTypes.cpp 中,有相关的代码:

b; Z U Y h y e P :ool createIfNotFound = false;
const char16_t* resourceRefName;
int resourceNameLen;
if (len > 2 &&? ( 9 } S y ` + G; s[1] == '+K @ * N m B @ n z') {
createIfNotFound = true;
resourceRefNa2 . Ime = s + 2;
resourceNameLen = len - 2;
} else iS | | y 7 S v E Yf (le? g `n > 2 && s[1] == '*{ + ~ ! A G B') {
enforcePrivate = false;
resourceRef3 ) . GName = s + 2;
resourceNameLen = len - 2;
} else {
createIfNotFound = false;
resourceRefName = s + 1;
resourceNameLen = len - 1;
}
String16 package, tB 7 [ 6 . Jype, name= 7 ] Z;
if (!expandResourceRef(resourceRefNP o @ 7 L & )ame,resou^ [ A c 7 O ( ) -rceN2 ` 4 qameLen, &package, &type, &name,
defType, defPackag] Q = e - O - 5e, &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()m V U, packagv } qe.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(accesso( l R n p z - [ `rCookie, "ResZ E u -ource is notJ @ 2 a y f v public.y h F # J")w - s l V;
}
return false;
}
}= ^ a $
}
// ...
}
咱们检查上面相关的代码,知道只需关闭 eV 1 r RnforcePrivate 这个开关即可,检查这一段逻辑,能u 2 E – 8 j够很轻松得到定论,只需这样写就行了:
<?xml ve| V P p i C 0rsioU s } 9  U  Y Xn="1.0" encoding="utf-8"?>
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_pa] _ $ N F 4 Brent"
android:sr4 / o [ ^ ic="@*com.google.and( S : ` } % qroid.webview:drawa* / sble/icon_webU ( V # L Zview">
</ImageView>

留意 @ 和包名之间多了一个 *,K E V l T % m F这个星号,便是无视私有资源直接引证的意思,再一次运用 aapt 编译,资源编译成
功。检查编译出来的文件

Webview.apk —— Google 官方的私有插件化方案
看咱们的引证变成了 @dref/0x02060061 咦,packageId 怎样变成了 02,没联系,咱们后面的华章解开这个谜底。

DynamicRefTable

咱们依据刚刚上面的源码往下看,继N A u y续看 stringToVT G ) M alue 这个函数,会看见这么一段代码

if (accessor) {
rid = Res_MAK` 4 y D ? , F EEID(
accessor->getRemappedPackage(Res_GETPACKAGE(rid)),
Re` w h * D y Q !s_GETTYPE(rid), Res_GETENTRY(rid));
if (kD) M Y ] 3 UebugTableNoisy) {
ALD ( I e &OGI("Incl %s:%s/%s: 0x%08x\n",
String8(packE  - { kage).string(), String8(type).string(),
String8(name).string(), rid);
}
}
uint32_t packageId = Res_GETPACKAGE(rid) + 1;
if (packageId != APP_PACKAGE_IDY  k | L x 5 && packagh g 1eId != SYS_PACKAGE_ID) {
outValue-R - m v ; w>dataType = Res_value::TYPE_DYNAMIC_REFERENCE;
}
outValue->data = rid;

这段代码告知咱们几件事:

  1. 刚刚的 webview 的 packageIdS b ) } t n 6 是通过 remapp 后的

  2. 它的类型变成了 TYPEDYNAMICREFERENCE

看英文翻译是“动态引证”的意思。咱们运用 a & / E D @ ( rapt d--values resourcesout.apk指令把资源E z ?信息打印出来,能够发现

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

这儿有关的是一个 DynamicRefTable,看它里边的值,好像是 packageId 和 packageName 映射。也2 P j q便是说,0x02 的F } ^ 8 packag1 f } 6 ?eId 所在的资源,应该是在叫com.google.android.webview 的包里的。

咱们查询 TYPEDYNAMICREFERENCE 和 DynamicRefT& b D @able 有关的代码,找到了这么一个函数,咱们看下界说:

status_t DynamicRe! K .fTable::look: l d 1upReso6 s w y SurceId(uint32_t* resId) const {
uint32_t res = *resId;
size_t packageId = Res_GETPACKAGE(res) + 1;
if (packageId == APP_PACKAGE_ID g l l h = P [ - &H M ( l P xamp;& !mAppAsLib) {
// No lookup needs to be done, app package IDs are absolute.
return NO_ERROR;
}
if (p7 p uackageId == 0 || (packageId == APPm D W 8_PY : ] 1 Q O $ ^ACKAGE_ID && mAppAsLib)) {
// Th) S P N S + 3 1e package ID is 0x00. That means that a shN p Sared library is accessingc $ _ % m
// its ownO - C , ` S local resource.
// Or if app resource is loaded as share` F g H  Nd library, the resource which has
// app package Id is local resources.
// so we fix up those resources with the calling package ID.
*resId = (0xFFFFFF & (*re? [ F GsId)) | (((uint32_t) mAs] [ H k 5 RsignedPackageId) << 24);
ret$ ! Eurn NO_E 8 e , d T K f cERROR;
}
// Do a proper lookup.
uint8_t translatedId = mLookupTable[packageId];
if (translatedId == 0) {
ALOGW("Dynamp X 6icRefTablew B } B # d 1(0x%02x): No mapping for build-time package ID 0x%02x.",
(= O ) T % } r r Cuint8_t)mAssignedPackageId, (5 Q j v ! R . H $uint8_t)pack^ Z D H ?ageId);
for (size_t i = 0; i < 256; i++^ 8 j) {
if (mLookupTable[i] != 0) {
ALOGW("e[0x%02x] ->7 R y a; 0x%02x", (uint8_t)i, mLookupTable[i]);
}
}
retu& C & R 2 d d s 2rn UNKNOWN_ERROR;
}
*resId = (res & 0x00ffffff) | (R F f c 5 n z R ((uint32_t) translatedId) << 24);
return No & ^O_ERROR;
}

得到几个定论:

  1. 假如 packageId 是 0x7f 的话,不转化,本来的 ID 仍是本来的 ID

  2. 假如 packageId 是^ # % { , 4 0 或许 packageId 是 7f 且 mAppAsLib 是真的话,把 packgeId 换成 mAssig1 o X j L fnedPackageId

  3. 否则从 mLookupTA J 4able 这个表中做一个映射,换成 translatedId 回来。

条件= Q o一很明确,二的话应该是 webview.apk 拜访自己的资源情L Z D 3 y G : % 6况,暂时不论。条x s * d E 5 O件三便是咱们现在想要知道的场景了。
我对 mLookupTabe } u f y = [ Cle 这个变量非常猎奇,所以跟踪调用,检查界说,终究找到一些要害信息,在 AssetManager2 中找到相关代码,咱们给它添加额外的注释阐明

void AssR ( ( Y f } J ^ @etManager2::BuildDynamicRefTablw o ne() {
package_groups_.cleare o 6 O % q B();
package_ids_.fill(0xff);
// 0x01 is reserved for the android package.
int next_package_id = 0x02;
const size_t apk_assets_count = apk_f W h kassets_.size();
for (size_t i = 0; i < apk_assets_count; i++) {
const ApkAssets* apk_asset = apk_assets_[i];
for (const std::uni^ 8 P Cque] { s_ptr<consZ ^ Z d | Z ! :t LoadedPackage>& package :
apk_asset->GetLoadedArsc()->GetPackages()) {
// Get the package ID or assign one if a shared library.
int package_id;
if (package->IsDynamic()) {
//S @ { M m在 LoadedArsc 中,发现假如 packageId == 0,就被界说为 DynamK H E 7 Z j 7icPackage
package_id = next_pacH u e / x P t /kage_id++;
} ec P Else {
//否则运用自己界说的 packageId (非0)
package_id = package->GetPackageId(`  F);
}
// Add the mapping for package ID to index if not pM l %resent.
uint8_t idx = package_ids_[package_id];
if (idx == 0xff) {
// 把这个 packageId 记录下来,并赋值进内存中和 pa, k 7 5 a .ckage 绑定起来
package- 0 G_ids_[package_id] = idx = static_cast<uint8_t>(package_groups_.size());
package_groups_.push_back({});
package_groups_.back().dynamic_ref_table.mAssignedPackageId = package_id;
}
Pac] | 0 O & M H Z ^kageGroup* package_group =- , p Y &package_groups. R Z U_[idx];
// Add the package and to th[ C . k D @ ! .e set of packal h 9 5 oges with the same ID.
package_group-&F & Q t 5 - V *gt;packages_i g U n J } | 0.push_back(package.T ] F = a h z [ vget());
package_group->cookies_.push_back(static_cast<ApkAssetsCookie>(i));
// 一起更改 DynaU w YmicRefTable 中 包名 和 ph l O k , D tackageId 的对应联系
// Add the package name -> buiB [ . | /ld time ID mappings.
for (cons( , p 3 I X B &t DynamicPackageEntry& entry : package->GetDynamicPackD Z * Y M g xageMap()) {
String16 package_name(entry.package_ # p G = wname.P o ) Z 6 ( Y B cc_str(), entry.package_name.size());
package_group->dynamic_ref_table| ) R V g 3.S 2 . j ! a / ImEntries.replaceValueFor(
package_name,/ { a v ! ~ & static_cast<uint8_t>(entry.package_id));
}
}
}
// 运用 O(na R Y j^2) 的方法,把现已缓存的一切 DynamicRefTable 中的 包名 -> id 的联系悉数重映射一遍
// Nowd G X P F 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 != p3 n `ackage_groups_ee ~ Q ? C ) d `nd; ++iter) {
const std::string& pa/ Y m rckage_name = iter->packages_[0]->GetPackageName();
for (auto iter2 = package_groups_.begin(); iter2 != package_groups_end; ++iter2) {
iter2->dynamic_ref_tablu | ] 5 B { R L Ce.addMapping(SW ? o ~tring16(package_name.c_str(I 8 L _), package_name.size()),
iter->dy% | J / znamc ) o I & ( & $ /ic_ref_table.mAssignedPackageI` # H *d);
}
}
}

上面的中文注释是我加的,这一段逻辑其实很简单,咱们通过这样的处理,完成了 buildId -> runtimeId 的映射。也便是说,WebView 的 packageId 是在运转时动态计算生成的!

这样的的确确处理了 packageId 保护的问题,因为 pacakgeId 能够重置,咱们只需保护 packL 8 / A z (ageName 就行了。

总结

通过以上的调研,咱们目h b V e前知道了Google 官方的“插件化资源”是如何完成的。可是这个计划也有一个弊端,便是在 5.0 以下的手机上会 crash,原因是 5.0 以下的体系并不知道 TYP– G t c a 0 oEDYNAMICREFERENCE 这个类型。因而假如你的 App 还需求支撑 5.0 以下的运用的话,还需求通过一些修改才干完成:
  1. 仍然需求手动办理 packageId。

  2. 把 aapt 中关于 d^ T Hynamic refeP i 2 7 / Crence 的当地改成 reference。

等待各大厂商在努力更新 Androk I ` Oid 版别上能迈出更大的脚步,一旦 5.0 以下的手机绝迹,我相信咱们的 Android App 生态也会变得愈加夸姣。
Webview.apk —— Google 官方的私有插件化方案
– – – – – – END – – – – – –
Webview.apk —— Google 官方的私有插件化方案
Webview.apk —— Google 官方的私有插件化方案