Androidステップアップ(八)熱修復の基本原理

10058 ワード

一、コード修復
1、クラスロード方案
(1)Dexパケット原理
1つのDexファイル内のメソッド数は65536メソッドを超えてはいけません.
(1)理由:androidはクラスごとのメソッドidを検索するため,チェーンテーブル構造の中に存在する.しかし、このチェーンテーブルの長さは1つのshortタイプで保存され、shortは2バイト(保存-2の15回から2の15回-1、すなわち-32768~32767)を占め、最大保存数は65536である.
(2)解決策:
  • メソッド数を簡略化し、使用していないクラス、メソッド、サードパーティライブラリを削除します.
  • ProGuardを使用して未使用のコードを削除
  • は、一部のモジュールに対してローカルプラグイン化された方式を採用している.
  • 分割Dex
  • Dex下請け方式は主にパッケージ化時にアプリケーションコードを複数のDexに分割し、アプリケーション起動時に使用しなければならないクラスとこれらのクラスの直接参照クラスをメインDexに、他のコードをセカンダリDexに配置する.アプリケーションが起動すると、プライマリDexがロードされ、アプリケーションが起動するまで動的にセカンダリDexがロードされます.
    (2)クラスロード修正案
    キーならClassファイルに異常がある、そのClassファイルを修復してPatchに打ち込む.dexのパッチパッケージ(1)の一態様:PathClassLoaderに取得するDexPathListを反射により取得し、その後DexPathListにおけるElement配列を取得する、Patch.dexはElement配列dexElementsの最初の要素に配置され、最後に配列をマージして再設定します.クラスローディングを行う場合、ClassLoaderの親依頼メカニズムにより、このクラスは一度だけローディング、つまりPatch.dexのキーClassがロードされます.(2)方案二:
    dex差分パケットpatchを提供する.dex、patch.dexとアプリケーションのclasses.dexは完全なdexに結合され、完全なdexがロードされてdexFileオブジェクトが得られ、パラメータとしてElementオブジェクトが構築され、古いdex-Filements配列が全体的に置き換えられます.(Tinker)
    (3)クラスロード方式の制限
    シナリオ1:
  • クラスはアンインストールできないため、クラスが再ロードする必要がある場合はAppを再起動する必要があるため、クラスロード修復スキームは即時に有効ではありません.
  • ARTモードでは、クラスが構造を変更するとメモリエラーが発生します.この問題を解決するためには、関連する呼び出しクラス、親クラスの子クラスなどすべてpatchにロードする必要がある.dexでは、パッチパッケージが大きく、時間がかかります.

  • シナリオ2:
  • 次回起動修復
  • dexマージメモリ消費はOOMを引き起こす可能性があり、最終的にdexマージに失敗した
  • 2、下位代替案
    (1)基本案
    主にNativeレイヤで既存のメソッドを置き換え,ArtMethod構造体には実行エントリ,アクセス権,所属クラス,コード実行アドレスなどJavaメソッドのすべての情報が含まれている.ArtMethod構造体のフィールドを置換するか、ArtMethod構造体全体を置換するかは、最下位の置換スキームです.メソッドを直接置き換えることで、再起動する必要がなくなります.
    (2)メリットとデメリット
    (1)欠点
  • では、既存のクラスのメソッドとフィールドを増減できません.メソッド数を増やすと、メソッドインデックス数も増加します.これにより、メソッドにアクセスするときにインデックスで正しいメソッドを見つけることができません.
  • プラットフォームの互換性の問題は、メーカーがArtMethod構造体を修正した場合、置換メカニズムに問題があります.

  • (2)メリット
  • Bug修復の即時性
  • で生成するPATCHは体積が小さく、性能の影響が低い
  • である.
    二、資源修復
    1、Instant Run
    コアコード:runtime/MonkeyPatcher.java
    #MonkeyPatcher
    public static void monkeyPatchExistingResources(@Nullable Context context,
                                                    @Nullable String externalResourceFile,
                                                    @Nullable Collection activities) {
        ......                                        
        try {
            // Create a new AssetManager instance and point it to the resources installed under
            // (1)         newAssetManager,  addAssetPath   sdcard     
            AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
            Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            mAddAssetPath.setAccessible(true);
            if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
                throw new IllegalStateException("Could not create new AssetManager");
            }
            // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
            // in L, so we do it unconditionally.
            Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
            mEnsureStringBlocks.setAccessible(true);
            mEnsureStringBlocks.invoke(newAssetManager);
            if (activities != null) {
                //(2)    Activity AssetManager   ,       newAssetManager
                for (Activity activity : activities) {
                    Resources resources = activity.getResources();
                    try {
                        Field mAssets = Resources.class.getDeclaredField("mAssets");
                        mAssets.setAccessible(true);
                        mAssets.set(resources, newAssetManager);
                    } catch (Throwable ignore) {
                        Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                        mResourcesImpl.setAccessible(true);
                        Object resourceImpl = mResourcesImpl.get(resources);
                        Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                        implAssets.setAccessible(true);
                        implAssets.set(resourceImpl, newAssetManager);
                    }
                    Resources.Theme theme = activity.getTheme();
                    try {
                        try {
                            Field ma = Resources.Theme.class.getDeclaredField("mAssets");
                            ma.setAccessible(true);
                            ma.set(theme, newAssetManager);
                        } catch (NoSuchFieldException ignore) {
                            Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
                            themeField.setAccessible(true);
                            Object impl = themeField.get(theme);
                            Field ma = impl.getClass().getDeclaredField("mAssets");
                            ma.setAccessible(true);
                            ma.set(impl, newAssetManager);
                        }
                    ......
            }
            //(3)  Resource      , AssetManager   newAssetManager
            for (WeakReference wr : references) {
                Resources resources = wr.get();
                if (resources != null) {
                    // Set the AssetManager of the Resources instance to our brand new one
                    try {
                        Field mAssets = Resources.class.getDeclaredField("mAssets");
                        mAssets.setAccessible(true);
                        mAssets.set(resources, newAssetManager);
                    } catch (Throwable ignore) {
                        Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                        mResourcesImpl.setAccessible(true);
                        Object resourceImpl = mResourcesImpl.get(resources);
                        Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                        implAssets.setAccessible(true);
                        implAssets.set(resourceImpl, newAssetManager);
                    }
                    resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
                }
            }
        } catch (Throwable e) {
            throw new IllegalStateException(e);
        }
    }
    
  • 反射新しいAssetManagerを構築し、addAssertPathを呼び出してsdcardにロードする新しいリソースパッケージを反射して、すべての新しいリソースを含むAssetManager
  • を得る.
  • 元はAssetManagerに参照する場所を、反射により参照先を新しいAssetManager
  • に置き換える.
    2、リソースパッケージ置換(Sophix)
    デフォルトでAndroid SDKでコンパイルされたapkは、そのリソースパッケージのpackage idが0 x 7 fです.framework-res.jarのリソースpackage idは0 x 01
  • はpackage idが0 x 66のリソースパケット(0 x 7 fおよび0 x 01以外)を構築し、変更されたリソース項目のみを含む.
  • すでにロードされているOx 7 fと競合しないため、このパッケージは既存のAssetManagerのaddAssetPathでロードできます.

  • 三、SOライブラリの修復
    本質はnative法の修復と置換である.
    1、soライブラリのロード
    (1)以下の方法でsoライブラリをロードする
    #System
    public static void loadLibrary(String libname) {
        Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
    }
       so   ,  apk lib   
    
    public static void load(String filename) {
        Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
    }
           so   ,   so          
    
    private static native String nativeLoad(String filename, ClassLoader loader, String librarySearchPath);
    

    最終的にはnativeメソッドnativeLoadが呼び出され、パラメータfileNameはディスク内のsoの完全なパス名です.
    (2)nativeLibraryDirectoriesディレクトリの遍歴
    #DexPathList
    public String findLibrary(String libraryName) {
        String fileName = System.mapLibraryName(libraryName);
        for (File directory : nativeLibraryDirectories) {
            File file = new File(directory, fileName);
            if (file.exists() && file.isFile() && file.canRead()) {
                return file.getPath();
            }
        }
        return null;
    }
    

    クラスロードのfindClassメソッドと同様に、配列内の各要素はsoライブラリに対応し、最終的にsoのパスを返します.配列の一番前にsoパッチを追加すると、メソッドを呼び出してsoライブラリをロードすると、パッチsoのパスが返されます.
    2、SO修復方案
    (1)インタフェース置換
    システムの代わりに方法を提供する.loadLibraryメソッド
  • パッチsoが存在する場合、apkインストールディレクトリの下のsoライブラリ
  • をロードすることなく、パッチsoライブラリをロードする.
  • パッチsoが存在しない場合はSystemを呼び出す.loadLibraryはインストールapkディレクトリの下のsoライブラリ
  • をロードします.
    (2)反射注入
    soライブラリをロードするとnativeLibraryDirectoriesが巡回するため
  • パッチsoライブラリのパスを反射によりnativeLibraryDirectories配列の一番前の
  • に挿入する.
  • nativeLibraryDirectoriesを巡回すると、パッチsoライブラリが戻されてロードされ、修復の目的
  • が達成されます.
    四、熱修復フレームワークの分析
  • 下位置換スキーム:アリのAndFix、HotFix
  • 類ロード方案:QQ空間パッチ技術、微信のTinker方案、お腹が空いたAmigo
  • の2つの組み合わせ:Sophix
  • 参考資料:
  • 主流熱修復方案分析
  • Android熱修復技術、あなたはどう選びますか?
  • 『Androidステップ復号』
  • 『Android熱修復技術原理を深く探求する』