プラグインアクティビティフレームワークを手描き

序文

Qihoo 360のリプラグイン、DidiのVirtualAPK、そして現在はVirtualAppなどのプラグインテクノロジーが2015年から開花しています。プラグインテクノロジーは市場で厳しいテストを受け、徐々に成熟しています。今日は、みんなでプラグインを実装します。 -in。アクティビティフレームワークを変更してください。お役に立てば幸いです。

プラグインの概念

プラグインは、4つの主要なコンポーネントを動的にロードするテクノロジーです。当初は65535の制限の問題を解決することでしたが、その後、Google

は、市場でプラグインを使用してインストールパッケージのサイズをある程度縮小し、プロジェクトのコンポーネント化を実現するという問題を具体的に解決するためにmultidexを発表しました。分離を容易にし、コンポーネント化の結合を減らすための個別のプロジェクト。問題

はもちろん、プラグインテクノロジーは、仮想マシンの存在により、熱バグ修正を実現できます。Java自体が任意のクラスの動的ロードをサポートします。Androidシステムが4つの主要なコンポーネントに制限を課しているだけです。リストにないコンポーネントを開こうとすると、クラッシュします。

いわゆるプラグインは、基本的にこの制限を回避するためのものであり、アプリケーションは4つの主要なコンポーネントを自由に開いて使用できます。

プラグインのビジネス価値

プラグイン化は、クラスのロードとリソースのロードの問題を解決することに他なりません。リソースのロードは、通常、リフレクションAssertManagerを介して行われます。クラスのロードによると、プラグイン化は、通常、静的プロキシメソッドとフックメソッドに分けられます。プラグイン化は、通常、使用されます。新しいバージョンのアプリケーションのカバレッジが遅いことを解決するため。問題。

4つの主要コンポーネントは動的にロードできるため、ユーザーはアプリケーションの新しいバージョンを手動でインストールする必要がありません。また、ユーザーに新しい機能やページを提供したり、ユーザーが感じることなくバグを修正したりすることもできます。

プラグインプロジェクトの構造

プラグイン開発プロセス

ステップ1:メインアプリプロジェクトをホストプロジェクトとして作成する

ステップ2:プラグインパッケージを開く責任があるプラグインプロジェクトとしてplugin_packageを作成します

ステップ3:4つの主要コンポーネントのライフサイクルの管理を担当するインターフェースプロジェクトlifecycle_managerを作成します

ステップ4:プラグインをインストールする

4.1アセット内のファイルを/ data / data / filesディレクトリにコピーします

    public static void extractAssets(Context context, String sourceName) {
        AssetManager am = context.getAssets();
        InputStream is = null;
        FileOutputStream fos = null;
        try {
            is = am.open(sourceName);
            File extractFile = context.getFileStreamPath(sourceName);
            fos = new FileOutputStream(extractFile);
            byte[] buffer = new byte[1024];
            int count = 0;
            while ((count = is.read(buffer)) > 0) {
                fos.write(buffer, 0, count);
            }
            fos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            closeSilently(is);
            closeSilently(fos);
        }

    }

4.2静的プロキシを介してDexClassLoaderを構築する

コンテキスト環境がないため、コンテキスト環境はホストによって提供される必要があります。DexClassLoaderにはプラグインが含まれています。

    // 获取插件目录下的文件
        File extractFile = mContext.getFileStreamPath(mApkName);
        // 获取插件包路径
        String dexPath = extractFile.getPath();
        // 创建Dex输出路径
        File fileRelease = mContext.getDir("dex", Context.MODE_PRIVATE);

        // 构建 DexClassLoader 生成目录
        mPluginClassLoader = new DexClassLoader(dexPath,
                fileRelease.getAbsolutePath(), null, mContext.getClassLoader());

HookメソッドはdexファイルをホストのDexClassLoaderにマージすることですが、AMSマニフェストファイル登録をバイパスするアクティビティはClassNotFuoundExceptionをスローするため、フックstartActivityとhandleResumeActivityが必要です。前者は実装が簡単で互換性が高く、プラグイン-inは分離されており、後者は互換性が低く、開発が容易ですが、複数のプラグインが同じクラスである場合、問題が発生します。ここでは静的プロキシが使用されます。

4.3リフレクションAssertManagerを介してリソースの読み込みを実現する

       try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method method = AssetManager.class.getMethod("addAssetPath", String.class);
            method.invoke(assetManager, dexPath);
            mPluginResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(),
                    mContext.getResources().getConfiguration());
        } catch (Exception e) {
            Toast.makeText(mContext, "加载 Plugin 失败", Toast.LENGTH_SHORT).show();
        }

ステップ5:プラグインを解析します

静的プロキシの実装は非常に単純です。アクティビティの起動プロセスに精通している必要はありません。インターフェイスプログラミングを直接対象としています。まず、ホストアプリにプラグインをロードして、DexClassCloderとリソースを構築する必要があります。 DexClassLoaderを使用すると、AssertManagerを反映することでプラグインにクラスResourceをロードできます。addAssertPathはAssertManagerを作成してから、Resourceオブジェクトを構築します。もちろん、Serviceの開始と動的ブロードキャストの登録は実際にはActivityの開始と同じです。すべてホストのコンテキストから開始されますが、DLフレームワークは静的ブロードキャストをサポートしていません。静的ブロードキャストは、アプリケーションのインストール時に解析および登録され、プラグインのマニフェストは登録できないため、内部の静的ブロードキャストは、リフレクションを使用してPackageParserのparsePackageメソッドを呼び出すことにより、手動でのみ解析および登録できます。動的ブロードキャストへのブロードキャスト。特定の実装は、PluginManager#parserApkActionメソッドの実装にあります。

 public void parserApkAction() {
        try {
            Class packageParserClass = Class.forName("android.content.pm.PackageParser");
            Object packageParser = packageParserClass.newInstance();
            Method method = packageParserClass.getMethod("parsePackage", File.class, int.class);
            File extractFile = mContext.getFileStreamPath(mApkName);
            Object packageObject = method.invoke(packageParser, extractFile, PackageManager.GET_RECEIVERS);
            Field receiversFields = packageObject.getClass().getDeclaredField("receivers");
            ArrayList arrayList = (ArrayList) receiversFields.get(packageObject);

            Class packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
            Class userHandleClass = Class.forName("android.os.UserHandle");
            int userId = (int) userHandleClass.getMethod("getCallingUserId").invoke(null);

            for (Object activity : arrayList) {
                Class component = Class.forName("android.content.pm.PackageParser$Component");
                Field intents = component.getDeclaredField("intents");
                // 1.获取 Intent-Filter
                ArrayList<IntentFilter> intentFilterList = (ArrayList<IntentFilter>) intents.get(activity);
                // 2.需要获取到广播的全类名,通过 ActivityInfo 获取
                // ActivityInfo generateActivityInfo(Activity a, int flags, PackageUserState state, int userId)
                Method generateActivityInfoMethod = packageParserClass
                        .getMethod("generateActivityInfo", activity.getClass(), int.class,
                                packageUserStateClass, int.class);
                ActivityInfo activityInfo = (ActivityInfo) generateActivityInfoMethod.invoke(null, activity, 0,
                        packageUserStateClass.newInstance(), userId);
                Class broadcastReceiverClass = getClassLoader().loadClass(activityInfo.name);
                BroadcastReceiver broadcastReceiver = (BroadcastReceiver) broadcastReceiverClass.newInstance();
                for (IntentFilter intentFilter : intentFilterList) {
                    mContext.registerReceiver(broadcastReceiver, intentFilter);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

AssertManagerオブジェクトを使用すると、リソースファイルにアクセスできますが、プラグインにはコンテキストコンテキスト環境がありません。このコンテキスト環境は、ホストから提供される必要があります。具体的な方法は、プラグインのアクティビティアノテーションを取得することです。 -PackManagerを介して入口に入力し、ホストコンテキストを挿入します。これにより、ホストアプリの手順が完了し、プラグインアプリにジャンプします。ただし、プラグインアプリにはコンテキスト環境がないため、プラグインアプリは直接startActivityを実行できません。ホストのコンテキストstartActivityを取得する必要があります。

6番目のステップ、プロキシアクティビティ:lifecycle_mananagerにActivityInterfaceを構築して、プラグインアクティビティのライフサイクルを管理します

public interface ActivityInterface {

// 插入Activity上下文
    void insertAppContext(Activity hostActivity);

// Activity各个生命周期方法
    void onCreate(Bundle savedInstanceState);

    void onStart();

    void onResume();

    void onPause();

    void onStop();

    void onDestroy();
}

7番目のステップ、プロキシアクティビティ:plugin_packageにBaseActivityをビルドして、ActivityInterfaceを実装します

BaseActivityでstartActivityを提供し、それをホストアクティビティにスローして開始します

    public void startActivity(Intent intent) {

        Intent newIntent = new Intent();
        newIntent.putExtra("ext_class_name", intent.getComponent().getClassName());
        mHostActivity.startActivity(newIntent);
    }

8番目のステップ、プロキシアクティビティ:plugin_packageでアクティビティプラグインをビルドします

public class PluginActivity extends BaseActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    findViewById(R.id.btn_start).setOnClickListener(
                v -> startActivity(new Intent(mHostActivity, TestActivity.class))
        );
 }
}

// 测试插件Activity
public class TestActivity extends BaseActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
    }
}

ステップ9:プラグインのエントランスアクティビティを開始します

このステップの主なことは、プラグインのホストコンテキストを登録することです

   // PorxyActivity
protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       // 获取到真正要启动的插件 Activity,然后执行 onCreate 方法
       String className = getIntent().getStringExtra(EXT_CLASS_NAME);
       try {
           Class clazz = getClassLoader().loadClass(className);
           ActivityInterface activityInterface = (ActivityInterface) clazz.newInstance();
           // 注册宿主的 Context
           activityInterface.insertAppContext(this);
           activityInterface.onCreate(savedInstanceState);
       } catch (Exception e) {
           e.printStackTrace();
       }
   }

       @Override
    public void startActivity(Intent intent) {
        String className = intent.getStringExtra(EXT_CLASS_NAME);
        Intent proxyIntent = new Intent(this, ProxyActivity.class);
        proxyIntent.putExtra(EXT_CLASS_NAME, className);
        super.startActivity(proxyIntent);
    }

このようにして、PluginActivityの起動は完了しましたが、プラグインにはコンテキスト環境がないため、プラグインのアクティビティではこれを使用できなくなったことに注意してください。コンテキストの呼び出しでは、ホストのコンテキストを使用する必要があります。次のような実行:

BaseActivityでfindViewByIdを指定すると、レイアウトIDファイルを見つけることができます

    public View findViewById(int layoutId) {
        return mHostActivity.findViewById(layoutId);
    }

BaseActivityにsetContentViewを提供して、UIレイアウトのレンダリングを容易にします

    public void setContentView(int resId) {
        mHostActivity.setContentView(resId);
    }

プラグイン原理の概要

  1. DexClassLoaderを使用してプラグインのAPKをロードします

  2. エージェントのアクティビティを通じてプラグインのアクティビティを実行し、対応するライフサイクルをロードします

  3. リフレクションを介してAssetManagerのaddAssetPathを呼び出し、プラグインにリソースをロードします

プラグイン化で発生した問題

見つかったアクティビティはプラグインパッケージにありません

実際に開いたのは、プラグインパッケージで定義されたアクティビティです。このアクティビティに必要な情報は、ホストではなくプラグインパッケージにあります。

解決

プラグインアクティビティは、attachBaseContextメソッドも書き換えます。このステップでは、プラグインクラスローダーとResourcesインスタンスを使用して独自のコンテキストを作成し、ベースコンテキストをそれに置き換えて、ストレージ用の親クラスに渡します。このように、ビジネスがgetClassLoader()またはgetResources()を呼び出すと、すべてのプラグイン情報が取得されます。

リソースIDタイプの不一致が見つかりません

リソースIDでドローアブルを取得する必要がある場合、取得するのは色またはその他のリソースです

解決

主に8.0より前のバージョンで発生します。調査の結果、8.0より前のプラグインパッケージでは、ContextThemeWrapper.mResourcesがホストのリソースであり、プラグインのリソースではないことがわかりました。その結果、同じIDで見つかったリソースは対応していません。

プラグインパッケージのリークカナリアによるクラッシュ

リークカナリアは、スタックの最上位にあるアクティビティリソースを使用して、表示する画像を読み込みますが、このリソースは現在のプラグインにない可能性があります。

解決

ホストとすべてのプラグインはleakcanaryに依存しています。

総括する

この記事は主に、私が実際に本番環境に導入したプラグインコンポーネント化の私自身の実践に基づいており、SDKプラグインを動的にロードするときに考慮する必要のあるいくつかの問題を共有しています。コンテンツには主に、プラグインソリューションの一般的な問題、プラグインパッケージのリークカナリアによって引き起こされるクラッシュ、リソースIDタイプが一致しない、ホストアクティビティが問題を見つけることができないなどが含まれます。数千の単語が1つにまとめられます。文:

プラグインにはリスクが伴います。投資には注意が必要です。


著者:小さな木箱の
リンク:https://juejin.cn/post/6897476017527783437
出典:ナゲッツの
著作権は著者が保有しています。商用の再版の場合は、著者に連絡して許可を求めてください。非商用の再版の場合は、出典を示してください。

おすすめ

転載: blog.csdn.net/qq_39477770/article/details/110073904