blog

Webview.apk -- Google公式のプライベート・プラグイン・ソリューション。

Android携帯がバージョン5.0に移行した後、Android携帯を使うときに気づくかもしれない奇妙なことの1つは、WebViewがシステムに従うことなくアプリショップでアップグレードできることです...

Mar 27, 2020 · 11 min. read
シェア
Androidのバージョンが5.0を超えた後、Android携帯を使用する過程で、携帯電話内のWebViewがシステムに従わずにアプリショップでアップグレードできるという奇妙な現象を見つけることがあります。

iOSではまだ実装されていません。しかし、webview.apkは普通のapkではありません。 まず、アイコンがないので、クリックして起動する「アプリ」とはみなされません。同時に、このAPKをアップデートすると、webviewを使用しているすべてのアプリがアップデートされ、進む、戻るなどのwebviewのUIまでアップデートされます。

これはどのように機能するのでしょうか?今日はAPKウェブビューを分析します。

Android リソースID

Androidのパートナーを開発したことがある人なら、Rクラスに慣れているでしょう。Rクラスは、すべての「文字列」は理解できますが、16進数の束は、このようなR longを見るように、あまりなじみがないかもしれません:

public class R {
public static class layout {
public static final int activity_main = 0x7f
 }
}

最後の16進数は一般的にリソースIDと呼ばれ、Rに詳しい方なら、リソースIDにはパターンがあることをご存知でしょう。

0xPPTTEEEE
PPはpackageId、TTはtypeId、EEEEはルールとして出てくるエンティティIDで、今日注目するのは最初の4つです。注意したことがある方ならご存知だと思いますが、一般的なPPの値は7Fです。

このモデルは、異なるシナリオで「同じ意味」のリソースを使うのにとても便利です。AndroidにはAssetManagerというクラスがあり、Rからid値を読み込み、resources.arscというテーブルで特定のリソースのパスや値を探してアプリに返す役割を担っています。resources.arscと呼ばれるテーブルで特定のリソースのパスまたは値を見つけ、アプリに返します。

プラグイン化におけるリソース固定

よく耳にするAndroidのプラグインソリューションに、固定IDという概念がありますが、これはどういう意味でしょうか?例えば、あるアプリが0x7f0103というidのリソースにアクセスしたとします。古いコードが再びこのリソースにアクセスすると、0x7f0103にアクセスし、得られるのはもはやイメージではなく文字列となり、アプリのクラッシュは壊滅的なものとなります。

そのため、リソース ID が一度生成されると、それを移動させることはできないということが期待されています。もしpackageIdが常に7fであるなら、packgeIdを変更する何らかの解決策があることを知るだけでは明らかに不十分で、異なるビジネスパッケージで異なるpackageIdを使用するだけで、id衝突の問題を大幅に回避でき、外部リソースのプラグイン使用の条件を提供できます。

などなど!冒頭で、webview.apkの更新 - コード、リソースの更新 - について話しました。これはプラグイン化の一種のように聞こえますが、Googleは開発者の知らないところでどのようにwebviewのプラグイン化を実装しているのでしょうか?この謎の層を明らかにすれば、このプラグイン機能を使用することも可能なのでしょうか?

もちろん、答えはイエスです。

WebView APKとアンドロイドのシステムリソース

Androidのツールチェーン開発者として、webviewに興味を持ち始め、webview.apkをダウンロードしたときに最初にしたことは、このAPKのどこが違うのかを確認するためにAndroid Studioにドラッグすることでした。

よく見ると、そのリソースのpackgeIdは00です!私の直感では、値0は特別なものです。

自慢のandroid sdkのandroid.jarが提供するリソースを見てみましょう。

余談ですが、@android:color/redのようなアンドロイドのシステムリソースを使うことは、実際にはandroid.jarで提供されているリソースを使うことになります。このandroid.jarをandroid.apkにリネームしてAndroid Studioにドラッグすると表示できます。

直感は私に、1この値も非常に特別な、この01の実装では、実際には、推測することによっても行う方法を知っている - リソースpackageIdのandroid.jarは01です参照してください予約IDとしてpackageId 01は、リソースのidのアンドロイドシステムは永久に固定されています!その後、すべてのアプリは、例えば、リソースの色/黒をチェックしに行く0x0106000cである上記の表の結果を確認し、私は少なくとも@アンドロイドの私のバージョンのすべてのアンドロイド携帯電話であることを確認して、0x01で始まるリソースは、常にOKになります得る:色/黒リソースのIDはすべて0x0106000cです。0x0106000c.私はそれを証明するためにデモを行うことができます、私はxmlファイルをコンパイルします:

<?xml version="1.0" encoding="utf-8"?>
<ImageView
xmlns:android="http://..///id"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
</ImageView>

コンパイルされた結果をご覧ください。

android:backgroundの値が@ref/0x0106000cに変わっていますね。 このapkがAndroid携帯で実行されると、AssetsManager内に2つのリソースパッケージが読み込まれます。1つは独自のアプリリソースパッケージで、もう1つはandroidフレームワークリソースパッケージです。0x0106000cを探すと、システムのリソースが見つかります。

android.jarは、特別な01は問題ありません、その後、システム内の多くのapkがある場合、それらの値は、2、3、4、5、......カオスの世界にそれについて考える、これが本当であれば、彼らはどのようにこれらのリソースを管理するパッケージそれを?

これらの好奇心を胸に、私はaaptのソースコードをダウンロードし、真実の世界を探求する準備をしました。

すべてがわかるAAPTソースコード

ソースコードプロセスとコンパイルプロセスをダウンロードして話すことはありませんが、便利にデバッグするためには、aptのデバッグバージョンをコンパイルすることをお勧めします、意味合いは、-Oθを使用して最適化をオフにし、デバッグモードのコンパイルを使用することができ、私はバージョンが28.0.3バージョンアンドロイドを使用しています。

まず、ResourceType.hで定義されている0xPPTTEEEEである理由を知るために、R以下の値の定義を見てみましょう。

#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

最初の3行がidの定義で、最後の2行が特別なpackageIdファクトです。さて、01はシステムパッケージリソースとして識別され、7fはAppパッケージリソースとして識別されます。

xmlで他のリソースパッケージを参照する方法は、先頭に@を使うことです。したがって、ウェブビューでリソースを使用する必要がある場合、パッケージ名を指定する必要があります。 実は、@androidのandroidはandroid.jar内のリソースのパッケージ名です。android.jarのパッケージ形式を見て、図のpackageNameに注目してください:

これを理解した上で、ウェブビューのリソースを使用する方法は、以下の例のようになります:

<?xml version="1.0" encoding="utf-8"?>
<ImageView
xmlns:android="http://..///id"
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 ...
public.xmlファイルを使用したことがある方は、プライベートリソースという概念を持つのはライブラリだけでなく、apk全体で使用されるリソースも含まれることを明確にしておきたいと思います。ただし、aarのようにpublicタグは厳密には制限されていません。しかし、aarのようなpublicタグは厳密には制限されていません。

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://..///id"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@*com.google.android.webview:drawable/icon_webview">
</ImageView>

これはプライベートリソースへの直接参照を無視し、もう一度 aapt を使ってコンパイルすることで、リソースが正常にコンパイルされたことを意味します。コンパイルされたファイルの表示

参照先は @dref/0x02060061 となります。

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;

このコードはいくつかのことを物語っています:

英訳すると「動的参照」。 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;
}

結論をいくつか出してください:

  1. packageIdが0x7fの場合、変換しなくても元のIDのままです。

  2. そうでない場合は、mLookupTableテーブルからマップを作成し、translatedIdとして返します。

条件1は明確で、条件2はとりあえずwebview.apkが自身のリソースにアクセスしているケースであるはずです。条件3は今知りたいシナリオです。
mLookupTable変数について興味があったので、呼び出しをトレースし、定義を見て、最終的にいくつかの重要な情報を見つけました。
 
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 
 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_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 2つのアプリの関係はすべてリマップされている。
 
// 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);
 }
 }
}
packageNameが維持されている限り、pacakgeIdはリセットできるので、これはpackageIdを維持する問題を解決します。

結論

以上の調査の結果、Google公式の「プラグイン・リソース」がどのように実装されているかがわかりました。なぜなら、5.0未満のシステムはTYPEDYNAMICREFERENCEのタイプを知らないからです。そのため、もしあなたのアプリが5.0未満のアプリをまだサポートする必要がある場合、動作させるためにいくつかの修正を加える必要があります:
5.0未満の携帯電話がなくなれば、Androidアプリのエコシステムはさらに良くなると確信しています。
























Read next

Ant Design v4+CracoでIconノードを動的に生成するには?

当時、私は操作に苦労しましたが、達成できませんでした。 学習は、学習と忘却のプロセスであり、新しい技術の追求は、基本的なことを忘れている、ああ、モーニングコールを記録するべきではありません。

Mar 27, 2020 · 2 min read