這一點在 iOS 中還沒有實現,(iOS OTA 的歷史也不是特別的悠久)。可是 webview.apk 不是一個普普統統的 apk,首先它沒有圖標,不算是點擊啓動的「App」。同時,更新這個 APK,會讓全部使用 webview 的應用都獲得更新,哪怕是 webview 中的 UI ,好比前進後退也同樣,獲得更新。
html
這一點是如何作到的呢?今天咱們來分析下 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
咱們知道 android 針對不一樣機型以及不一樣場景,定義了許許多多 config,最經典的多語言場景:values/values-en/values-zh-CN 咱們使用一個字符串資源可能使用的是相同的 ID,可是拿到的具體值是不一樣的。這個模型就是一個表模型 —— id 做爲主鍵,查詢到一行數據,再根據實際狀況選擇某一列,一行一列肯定一個最終值:bash
這種模型對咱們在不一樣場景下須要使用「同一含義」的資源提供了很是大的便捷。Android 中有一個類叫 AssetManager 就是負責讀取 R 中的 id 值,最終到一個叫 resources.arsc 的表中找到具體資源的路徑或者值返回給 App 的。cookie
所以,咱們指望資源 id 一旦生成,就不要再動來動去了。可是這裏又有一個很是顯眼的問題:若是 packageId 永遠是 7f,那麼顯然是不夠用的,咱們知道有必定的方案能夠更改 packgeId,只要在不一樣業務包中使用不一樣的 packageId,這樣能極大避免 id 碰撞的問題,爲插件化使用外部資源提供了條件。app
答案固然是確定的。ide
咱們再看下大名鼎鼎的 android sdk 中的 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 的時候,就會找到系統的資源裏面去。
帶着這些好奇,我下載了 aapt 的源碼,準備在真相世界裏一探究竟。
咱們首先能夠先瞅一眼,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 包資源。
知道這點之後,咱們使用 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/layoutactivity.xml:2: error: Error: Resource is not public. (at 'src' with value '@com.google.android.webview:drawable/iconwebview').
在使用 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;
}
}
}
// ...
}複製代碼
<?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 編譯,資源編譯成 功。查看編譯出來的文件
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;複製代碼
這段代碼告訴咱們幾件事:
剛剛的 webview 的 packageId 是通過 remapp 後的
它的類型變成了 TYPEDYNAMICREFERENCE
aapt d--values resourcesout.apk
命令把資源信息打印出來,能夠發現
咱們查詢 TYPEDYNAMICREFERENCE 和 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;
}複製代碼
獲得幾個結論:
若是 packageId 是 0x7f 的話,不轉換,原來的 ID 仍是原來的 ID
若是 packageId 是 0 或者 packageId 是 7f 且 mAppAsLib 是真的話,把 packgeId 換成 mAssignedPackageId
不然從 mLookupTable 這個表中作一個映射,換成 translatedId 返回。
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。
把 aapt 中關於 dynamic reference 的地方改爲 reference。