Proxy思想に基づくAndroidプラグインフレーム

72231 ワード

http://zjmdp.github.io/2014/07/22/a-plugin-framework-for-android/
本論文のコードは全部Githubにあります.android-plugin
意味
プラグインのフレームワークの意味は以下の点にあります.
  • は、インストールパッケージの体積を減少させ、ネットワークを介して選択的にプラグインの下で
  • を発行する.
  • はモジュール化され、ネットワークトラフィックを低減する
  • .
  • サイレントアップグレード、ユーザが感知しない場合にアップグレードする
  • .
  • 低バージョンの機種の方法数が制限を超えてインストールできない問題を解決しました.
  • コードデカップリング
  • 現状
    Androidではプラグインのフレームワークに関する技術について多くの議論と実装がなされており、プラグインは通常、appkまたはdexとしてパッケージ化されている.
    dex形式のプラグインはしばしば、JavaのJar形式と似ている機能的なインターフェースを提供します.AndroidのDalvik VMはJavaのByte Codeを直接動的にロードできないため、Dalvik Byte Codeを提供する必要があります.dexはDalvik Byte Code形式のjarです.
    appk形式のプラグインは、dex形式よりも多くの機能を提供しており、例えばリソースをappkにパッケージ化しても良いし、プラグイン内のActivityやServiceなどのシステムコンポーネントを実現しても良い.
    本稿では主にappk形式のプラグインフレームについて議論し、appk形式についてはインストールとインストールの2つの方法がある.
  • は、appをインストールする方式で比較的簡単に実現されています.主な原理は、プラグインappとメインプログラムを共有することによって、メインプログラムはcreatePackageContextを介してプラグインのcontextを構築し、contextを通じてプラグインappのリソースにアクセスできます.この方法の欠点は、ユーザーが手動でインストールする必要があります.体験はあまり良くないです.
  • は、appkをインストールしないで、ユーザーが手動でインストールした欠点を解決しました.しかし、実現するのは複雑で、主にDexClassloaderによって実現されます.同時に、プラグインのActivityなどのAndroidシステムコンポーネントをどうやって起動するかを解決します.プラグインフレームの本当の難点はここにあります.
  • DexClass loader
    ここでは「Java仮想マシンを深く理解する:JVM高級特性とベストプラクティス」の第二版の中でjava類キャリアに対する一部の説明を引用します.
    仮想マシン設計チームは、クラスローディング段階の「1つのクラスの全限定名来を通じて、このようなバイナリバイトストリームを記述する」という動作をJava仮想マシンの外部に実行して、アプリケーション自身が必要なクラスをどのように取得するかを決定する.この動作を実現するコードモジュールを「クラスキャリア」と呼びます.
    Android仮想マシンの実装は、javaのJVMを参照しているので、Androidにおいても、クラス加載器の概念を使用している.ただし、JVMにおける加載器ローディングclassファイルに対して、AndroidのDalvik仮想マシンはDexフォーマットであり、Dexローディングを完了するのは主にPathClassloaderおよびDexclassloaderである.PathClassloaderは、デフォルトでは/data/dalvik-cacheにキャッシュされたdexファイルを読み、インストールされていないappkがPathClassloaderでロードされると、/data/dalvik-cacheディレクトリの下で対応するdexが見つからないので、ClassNotFoundExceptionを抛り出す.DexClassloaderは、任意の経路の下にdexとappkファイルを含む、odex生成の経路を指定することにより、インストールされていないappkファイルをロードすることができる.次のコードは、DexClassloaderの使用方法を示している.
    final File optimizedDexOutputPath = context.getDir("odex", Context.MODE_PRIVATE);
    try{
        DexClassLoader classloader = new DexClassLoader("apkPath",
                optimizedDexOutputPath.getAbsolutePath(),
                null, context.getClassLoader());
        Class<?> clazz = classloader.loadClass("com.plugindemo.test");
        Object obj = clazz.newInstance();
        Class[] param = new Class[2];
        param[0] = Integer.TYPE;
        param[1] = Integer.TYPE;
        Method method = clazz.getMethod("add", param);
        method.invoke(obj, 1, 2);
    }catch(InvocationTargetException e){
        e.printStackTrace();
    }catch(NoSuchMethodException e){
        e.printStackTrace();
    }catch(IllegalAccessException e){
        e.printStackTrace();
    }catch(ClassNotFoundException e){
        e.printStackTrace();
    }catch (InstantiationException e){
        e.printStackTrace();
    }
    DexClassloaderは、クラスのロード問題を解決しました.プラグインappkにいくつかの簡単なAPI呼び出しがある場合、上のコードは需要を満たすことができますが、ここで論じたプラグインフレームは、リソースアクセスとAndroidシステムコンポーネントの呼び出しを解決する必要があります.
    プラグイン内のシステムコンポーネントの呼び出し
    Android FraamewarkにはActivityServiceContent ProviderBroadcastReceiverなどの4つのシステムコンポーネントが含まれています.ここでは主にメインプログラムでプラグインのActivityを起動する方法について議論しています.他の3つのコンポーネントの呼び出し方法は同様です.
    ActivityがAndroid Manifest.xmlで声明を出す必要があることはよく知られていますが、appkはインストール時PackageManagerServiceでappkのAndroid dManifest.xmlファイルを解析します.この時、プログラムに含まれているActivityはどれかを決定しました.
    起動プラグインの中のActivityは必ずメインプログラムのAndroid Manifest.xmlでこのActivityを宣言しますが、プラグインのフレームの柔軟性を確保するために、どのようなActivityがプラグインにあるかは予知できません.
    上記の問題を解決するために、ここではProxy思想に基づく解決方法を紹介します.大体の原理は、メインプログラムのAndroid Manifest.xmlにおいて一部のActivityNotFoundを宣言し、プラグインのActivityを起動すると、メインプログラムの一つProxyActivityに変わります.最後の効果は起動のこのActivityは実際にメインプログラムで宣言されたActivityですが、関連コードはプラグインActivityのコードです.これはプラグインActivityが宣言されていないと起動できない問題を解決しました.上から見て起動するのはプラグインのActivityです.全体の過程を具体的に分析します.
    Plugin SDK
    すべてのプラグインおよびメインプログラムは、PluginSDKに依存して開発される必要があり、すべてのプラグインのActivityは、PlugiSDKのProxyActivityから継承され、ProxyActivityPluginBaseActivityから継承され、PluginBaseActivityインターフェースを実現している.
    public interface IActivity {
        public void IOnCreate(Bundle savedInstanceState);
    
        public void IOnResume();
    
        public void IOnStart();
    
        public void IOnPause();
    
        public void IOnStop();
    
        public void IOnDestroy();
    
        public void IOnRestart();
    
        public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo);
    }
    public class PluginBaseActivity extends Activity implements IActivity {
        ...
        private Activity mProxyActivity;
        ...
        
        @Override
        public void IInit(String path, Activity context, ClassLoader classLoader) {
            mProxy = true;
            mProxyActivity = context;
    
            mPluginContext = new PluginContext(context, 0, path, classLoader);
            attachBaseContext(mPluginContext);
        }
        
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            if (mProxy) {
                mRealActivity = mProxyActivity;
            } else {
                super.onCreate(savedInstanceState);
                mRealActivity = this;
            }
        }
    
        @Override
        public void setContentView(int layoutResID) {
            if (mProxy) {
                mContentView = LayoutInflater.from(mPluginContext).inflate(layoutResID, null);
                mRealActivity.setContentView(mContentView);
            } else {
                super.setContentView(layoutResID);
            }
        }
    
        ...
    
        @Override
        public void IOnCreate(Bundle savedInstanceState) {
            onCreate(savedInstanceState);
        }
    
        @Override
        public void IOnResume() {
            onResume();
        }
    
        @Override
        public void IOnStart() {
            onStart();
        }
    
        @Override
        public void IOnPause() {
            onPause();
        }
    
        @Override
        public void IOnStop() {
            onStop();
        }
    
        @Override
        public void IOnDestroy() {
            onDestroy();
        }
    
        @Override
        public void IOnRestart() {
            onRestart();
        }
    }
    public class ProxyActivity extends Activity {
        IActivity mPluginActivity;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            Bundle bundle = getIntent().getExtras();
            if(bundle == null){
                return;
            }
            mPluginName = bundle.getString(PluginConstants.PLUGIN_NAME);
            mLaunchActivity = bundle.getString(PluginConstants.LAUNCH_ACTIVITY);
            File pluginFile = PluginUtils.getInstallPath(ProxyActivity.this, mPluginName);
            if(!pluginFile.exists()){
                return;
            }
            mPluginApkFilePath = pluginFile.getAbsolutePath();
            try {
                initPlugin();
                super.onCreate(savedInstanceState);
                mPluginActivity.IOnCreate(savedInstanceState);
            } catch (Exception e) {
                mPluginActivity = null;
                e.printStackTrace();
            }
        }
        
        @Override
        protected void onResume() {
            super.onResume();
            if(mPluginActivity != null){
                mPluginActivity.IOnResume();
            }
        }
    
        @Override
        protected void onStart() {
            super.onStart();
            if(mPluginActivity != null) {
                mPluginActivity.IOnStart();
            }
        }
        
        ...
        
        private void initPlugin() throws Exception {
            PackageInfo packageInfo = PluginUtils.getPackgeInfo(this, mPluginApkFilePath);
    
            if (mLaunchActivity == null || mLaunchActivity.length() == 0) {
                mLaunchActivity = packageInfo.activities[0].name;
            }
    
            ClassLoader classLoader = PluginUtils.getClassLoader(this, mPluginName, mPluginApkFilePath);
    
            if (mLaunchActivity == null || mLaunchActivity.length() == 0) {
                if (packageInfo == null || (packageInfo.activities == null) || (packageInfo.activities.length == 0)) {
                    throw new ClassNotFoundException("Launch Activity not found");
                }
                mLaunchActivity = packageInfo.activities[0].name;
            }
            Class<?> mClassLaunchActivity = classLoader.loadClass(mLaunchActivity);
    
            getIntent().setExtrasClassLoader(classLoader);
            mPluginActivity = (IActivity) mClassLaunchActivity.newInstance();
            mPluginActivity.IInit(mPluginApkFilePath, this, classLoader);
        }
        
        ...
        
        @Override
        public void startActivityForResult(Intent intent, int requestCode) {
            boolean pluginActivity = intent.getBooleanExtra(PluginConstants.IS_IN_PLUGIN, false);
            if (pluginActivity) {
                String launchActivity = null;
                ComponentName componentName = intent.getComponent();
                if(null != componentName) {
                    launchActivity = componentName.getClassName();
                }
                intent.putExtra(PluginConstants.IS_IN_PLUGIN, false);
                if (launchActivity != null && launchActivity.length() > 0) {
                    Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity));
    
                    pluginIntent.putExtra(PluginConstants.PLUGIN_NAME, mPluginName);
                    pluginIntent.putExtra(PluginConstants.PLUGIN_PATH, mPluginApkFilePath);
                    pluginIntent.putExtra(PluginConstants.LAUNCH_ACTIVITY, launchActivity);
                    startActivityForResult(pluginIntent, requestCode);
                }
            } else {
                super.startActivityForResult(intent, requestCode);
            }
        }
    ActivityおよびIActivityは、プラグインのフレーム全体のコアで、コードを簡単に分析する.
    まず、PluginBaseActivityを見てください.
    @Override
    protected void onResume() {
        super.onResume();
        if(mPluginActivity != null){
            mPluginActivity.IOnResume();
        }
    }
    変数ProxyActivityのタイプはProxyActivity#onResumeであり、プラグインActivityがmPluginActivityインターフェースを実現しているため、IActivityが最終的に実行したのはプラグインActivityのIActivityのコードであると推測され、以下にこの推測を確認する.mPluginActivity.IOnResume()onResumeインターフェースを実現していますが、これらのインターフェースは具体的にどのように実現されますか?コードを見る:
    @Override
    public void IOnCreate(Bundle savedInstanceState) {
        onCreate(savedInstanceState);
    }
    
    @Override
    public void IOnResume() {
        onResume();
    }
    
    @Override
    public void IOnStart() {
        onStart();
    }
    
    @Override
    public void IOnPause() {
        onPause();
    }
    
    ...
    インターフェースの実現はとても簡単で、インターフェースに対応するコールバック関数だけを呼び出しました.ここのコールバック関数は最終的にどこに移動しますか?前述したすべてのプラグインActivityはPluginBaseActivityから継承されます.つまり、ここのコールバック関数は最終的にプラグインActivityに対応するコールバックに変更されます.例えばIActivityが実行しているのはプラグインActivityのPluginBaseActivityのコードです.これも以前の推測を確認しました.
    上のコードの一部のセグメントは、プラグインの枠組みの核心的なロジックを示しています.他のコードは、このような論理的なサービスを実現するために、後はプロジェクト全体のソースコードを提供します.
    プラグイン内のリソースの取得
    プラグインappk内のリソースをロードするためのアイデアの一つは、プラグインappkのパスをメインプログラムリソース検索のパスに追加することであり、以下のコードは、この方法を示している.
    private AssetManager getSelfAssets(String apkPath) {
        AssetManager instance = null;
        try {
            instance = AssetManager.class.newInstance();
            Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            addAssetPathMethod.invoke(instance, apkPath);
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return instance;
    }
    プラグインActivityをリソースにアクセスさせるために、私たちがカスタマイズしたContectを使用します.IOnResumeの初期化にはいくつかの処理が必要です.
    public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo) {
        mProxy = true;
        mProxyActivity = context;
    
        mContext = new PluginContext(context, 0, mApkFilePath, mDexClassLoader);
        attachBaseContext(mContext);
    }
    onResumeにおいて、プラグインapp検索パスを含むContextは、PluginBaseActivityを再ロードすることによって実現される.
    public PluginContext(Context base, int themeres, String apkPath, ClassLoader classLoader) {
        super(base, themeres);
        mClassLoader = classLoader;
        mAsset = getPluginAssets(pluginFilePath);
        mResources = getPluginResources(base, mAsset);
        mTheme = getPluginTheme(mResources);
    }
    
    private AssetManager getPluginAssets(String apkPath) {
        AssetManager instance = null;
        try {
            instance = AssetManager.class.newInstance();
            Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            addAssetPathMethod.invoke(instance, apkPath);
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return instance;
    }
    
    private Resources getPluginAssets(Context ctx, AssetManager selfAsset)  {
        DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
        Configuration con = ctx.getResources().getConfiguration();
        return new Resources(selfAsset, metrics, con);
    }
    
    private Theme getPluginTheme(Resources selfResources) {
        Theme theme = selfResources.newTheme();
        mThemeResId = getInnerRIdValue("com.android.internal.R.style.Theme");
        theme.applyStyle(mThemeResId, true);
        return theme;
    }
    
    @Override
    public Resources getResources() {
        return mResources;
    }
    
    @Override
    public AssetManager getAssets() {
        return mAsset;
    }
    
    ...
    締め括りをつける
    本論文では、Proxy思想に基づくプラグインの枠組みを紹介します.コードはすべてGithubの中にあります.コードはフレーム全体の核心部分を抽出しただけです.もし生産環境に使うならば、PluginContextgetAssetsの部品のProxy類は実現されていません.ActivityのProxy実現も不完全です.同時に私もこの枠組みに致命的な欠陥がないと保証できません.本文は主に総括、学習と交流を目的としています.