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

在 Android 跨入 5.0 版本以後,咱們在使用 Android 手機的過程當中,可能會發現一個奇特的現象,就是手機裏的 WebView 是能夠在應用商店升級,而不須要跟隨系統的。html

這一點在 iOS 中還沒有實現,(iOS OTA 的歷史也不是特別的悠久)。可是 webview.apk 不是一個普普統統的 apk,首先它沒有圖標,不算是點擊啓動的「App」。同時,更新這個 APK,會讓全部使用 webview 的應用都獲得更新,哪怕是 webview 中的 UI ,好比前進後退也同樣,獲得更新。java

這一點是如何作到的呢?今天咱們來分析下 webview 這個奇特的 APK。android

Android 資源和資源ID

若是開發過 Android 的小夥伴,對 R 這個類是熟悉得不能再熟悉了,一個 R 類,裏面全部的「字符串」咱們都看得懂,可是一堆十六進制的數字,咱們可能並非很是的熟悉,好比看見一個 R 長這樣:web

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。cookie

咱們知道 android 針對不一樣機型以及不一樣場景,定義了許許多多 config,最經典的多語言場景:values/values-en/values-zh-CN 咱們使用一個字符串資源可能使用的是相同的 ID,可是拿到的具體值是不一樣的。這個模型就是一個表模型 —— id 做爲主鍵,查詢到一行數據,再根據實際狀況選擇某一列,一行一列肯定一個最終值:app

表結構

這種模型對咱們在不一樣場景下須要使用「同一含義」的資源提供了很是大的便捷。Android 中有一個類叫 AssetManager 就是負責讀取 R 中的 id 值,最終到一個叫 resources.arsc 的表中找到具體資源的路徑或者值返回給 App 的。ide

插件化中的資源固定

咱們常常聽見 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

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

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

這裏說個題外話,咱們使用 android 系統資源,好比 @android:color/red 這樣的方式,其實就是使用到了 android.jar 中提供的資源。咱們能夠把這個 android.jar 重命名成 android.apk,拖進 Android Studio 中進行查看。

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 的時候,就會找到系統的資源裏面去。

有一個 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.jar

知道這點之後,咱們使用 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」
公衆號

相關文章
相關標籤/搜索