Webviewapk-Google-官方的私有插件化方案

5次阅读

共计 10112 个字符,预计需要花费 26 分钟才能阅读完成。

在 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 作为主键,查问到一行数据,再依据理论状况抉择某一列,一行一列确定一个最终值:

这种模型对咱们在不同场景下须要应用“同一含意”的资源提供了十分大的便捷。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 到底有哪里不同。

认真看,它资源的 packgeId 是 00!直觉通知我,0 这个值很非凡。

咱们再看下赫赫有名的 android sdk 中的 android.jar 提供的资源。

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

咱们看到,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 的时候,就会找到零碎的资源外面去。

有一个 android.jar 是个非凡的 01 没问题,那如果零碎中存在许多的 apk,他们的值别离是 2,3,4,5,…… 想想都感觉要天下大乱了,如果这是真的,他们怎么治理这些资源 packageId 呢?

带着这些好奇,我下载了 aapt 的源码,筹备在假相世界里一探到底。

AAPT 源码,通知你所有

下载源码过程和编译过程就不讲了,为了调试不便,倡议大家编译出一个 没有优化 的 aapt debug 版,外延是应用 -O0 敞开优化,并应用 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 中的资源的形式就变成如下例子:

<?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/layout_activity.xml:2: error: Error: Resource is not public. (at ‘src’
with value ‘@com.google.android.webview:drawable/icon_webview’).

如果你之前应用过 public.xml 这个文件的话(你可能在这见过它:https://developer.android.com…),那么这里我须要阐明下 —— 不仅仅是 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 这个开关即可,查看这一段逻辑,能够很轻松失去论断,只有这样写就行了:

<?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 编译,资源编译成
功。查看编译进去的文件

看咱们的援用变成了 @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. 它的类型变成了 TYPE_DYNAMIC_REFERENCE

看英文翻译是“动静援用”的意思。
咱们应用 aapt d --values resources out.apk 命令把资源信息打印进去,能够发现

Package Groups (1)
Package Group 0 id=0x7f packageCount=1 name=test
  DynamicRefTable entryCount=1:
    0x02 -> com.google.android.webview

  Package 0 id=0x7f name=test
    type 1 configCount=1 entryCount=1
      spec resource 0x7f020000 test:layout/layout_activity: flags=0x00000000
      config (default):
        resource 0x7f020000 test:layout/layout_activity: t=0x03 d=0x00000000 (s=0x0008 r=0x00)
          (string16) "res/layout/layout_activity.xml"

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

咱们查问 TYPE_DYNAMIC_REFERENCE 和 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 还是原来的 ID
  2. 如果 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<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 保护的问题,因为 pacakgeId 能够重置,咱们只有保护 packageName 就行了。

总结

通过以上的调研,咱们目前晓得了 Google 官网的“插件化资源”是如何实现的。然而这个计划也有一个弊病,就是在 5.0 以下的手机上会 crash,起因是 5.0 以下的零碎并不意识 TYPE_DYNAMIC_REFERENCE 这个类型。因而如果你的 App 还须要反对 5.0 以下的利用的话,还须要通过一些批改能力实现:

  1. 仍然须要手动治理 packageId。
  2. 把 aapt 中对于 dynamic reference 的中央改成 reference。

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

欢送关注我的公众号「TalkWithMobile」

正文完
 0