在 Android 跨入 5.0 版本以後,咱們在使用 Android 手機的過程當中,可能會發現一個奇特的現象,就是手機裏的 WebView 是能夠在應用商店升級,而不須要跟隨系統的。html
這一點在 iOS 中還沒有實現,(iOS OTA 的歷史也不是特別的悠久)。可是 webview.apk 不是一個普普統統的 apk,首先它沒有圖標,不算是點擊啓動的「App」。同時,更新這個 APK,會讓全部使用 webview 的應用都獲得更新,哪怕是 webview 中的 UI ,好比前進後退也同樣,獲得更新。java
這一點是如何作到的呢?今天咱們來分析下 webview 這個奇特的 APK。android
若是開發過 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 的插件化的呢?若是咱們揭開了這一層神祕的面紗,咱們是否是也能夠用這個插件化的特性了呢?
答案固然是確定的。
我做爲一個 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 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,不要緊,咱們後面的篇章解開這個謎底。
咱們根據剛剛上面的源碼往下看,繼續看 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;
這段代碼告訴咱們幾件事:
看英文翻譯是「動態引用」的意思。
咱們使用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; }
獲得幾個結論:
條件一很明確,二的話應該是 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 如下的應用的話,還須要通過一些修改才能實現:
期待各大廠商在努力更新 Android 版本上能邁出更大的步伐,一旦 5.0 如下的手機絕跡,我相信咱們的 Android App 生態也會變得更加美好。
歡迎關注個人公衆號「TalkWithMobile」