天天看點

android webview_Webview.apk——Google 官方的私有插件化方案Android 資源和資源ID插件化中的資源固定WebView APK 和 android 系統資源AAPT 源碼,告訴你一切DynamicRefTable總結

本文由 Gemini Wen 創作

在 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 webview_Webview.apk——Google 官方的私有插件化方案Android 資源和資源ID插件化中的資源固定WebView APK 和 android 系統資源AAPT 源碼,告訴你一切DynamicRefTable總結

這種模型對我們在不同場景下需要使用“同一含義”的資源提供了非常大的便捷。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 到底有哪裡不同。

android webview_Webview.apk——Google 官方的私有插件化方案Android 資源和資源ID插件化中的資源固定WebView APK 和 android 系統資源AAPT 源碼,告訴你一切DynamicRefTable總結

仔細看,它資源的 packgeId 是 00!直覺告訴我,0 這個值很特殊。

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

這裡說個題外話,我們使用 android 系統資源,比如 @android:color/red 這樣的方式,其實就是使用到了 android.jar 中提供的資源。我們可以把這個 android.jar 重命名成 android.apk,拖進 Android Studio 中進行檢視。

android webview_Webview.apk——Google 官方的私有插件化方案Android 資源和資源ID插件化中的資源固定WebView APK 和 android 系統資源AAPT 源碼,告訴你一切DynamicRefTable總結

我們看到,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"?>
           

然後檢視編譯出來的結果

android webview_Webview.apk——Google 官方的私有插件化方案Android 資源和資源ID插件化中的資源固定WebView APK 和 android 系統資源AAPT 源碼,告訴你一切DynamicRefTable總結

我們看見 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:

android webview_Webview.apk——Google 官方的私有插件化方案Android 資源和資源ID插件化中的資源固定WebView APK 和 android 系統資源AAPT 源碼,告訴你一切DynamicRefTable總結

知道這點以後,我們使用 webview 中的資源的方式就變成如下例子:

<?xml version="1.0" encoding="utf-8"?>
           

我們執行下編譯,發現報錯了:

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/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 這個開關即可,檢視這一段邏輯,可以很輕松得到結論,隻要這樣寫就行了:

<?xml version="1.0" encoding="utf-8"?>
           

注意 @ 和包名之間多了一個 *,這個星号,就是無視私有資源直接引用的意思,再一次使用 aapt 編譯,資源編譯成功。檢視編譯出來的檔案

android webview_Webview.apk——Google 官方的私有插件化方案Android 資源和資源ID插件化中的資源固定WebView APK 和 android 系統資源AAPT 源碼,告訴你一切DynamicRefTable總結

看我們的引用變成了 @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",                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& 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 以下的系統并不認識 TYPE_DYNAMIC_REFERENCE 這個類型。是以如果你的 App 還需要支援 5.0 以下的應用的話,還需要經過一些修改才能實作:

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

期待各大廠商在努力更新 Android 版本上能邁出更大的步伐,一旦 5.0 以下的手機絕迹,我相信我們的 Android App 生态也會變得更加美好。

在這裡我也分享一份幾位大佬一起收錄整理的Android學習PDF+架構視訊+面試文檔+源碼筆記,進階架構技術進階腦圖、Android開發面試專題資料,進階進階架構資料

如果你有需要的話,可以私信我【資料】我發給你,歡迎大家來白嫖~

喜歡本文的話,不妨順手給我點個小贊、評論區留言或者轉發支援一下呗~
android webview_Webview.apk——Google 官方的私有插件化方案Android 資源和資源ID插件化中的資源固定WebView APK 和 android 系統資源AAPT 源碼,告訴你一切DynamicRefTable總結

繼續閱讀