基於Proxy思想的Android外掛程式架構

來源:互聯網
上載者:User

基於Proxy思想的Android外掛程式架構
 意義

研究外掛程式架構的意義在於以下幾點:

  • 減小安裝包的體積,通過網路選擇性地進行外掛程式下發模組化升級,減小網路流量靜默升級,使用者無感知情況下進行升級解決低版本機型方法數超限導致無法安裝的問題代碼解耦現狀

    Android中關於外掛程式架構的技術已經有過不少討論和實現,外掛程式通常打包成apk或者dex的形式。

    dex形式的外掛程式往往提供了一些功能性的介面,這種方式類似於java中的jar形式,只是由於Android的Dalvik VM無法直接動態載入Java的Byte Code,所以需要我們提供Dalvik Byte Code,而dex就是Dalvik Byte Code形式的jar。

    apk形式的外掛程式提供了比dex形式更多的功能,例如可以將資源打包進apk,也可實現外掛程式內的Activity或者Service等系統組件。

    本文主要討論apk形式的外掛程式架構,對於apk形式又存在安裝和不安裝兩種方式

    安裝apk的方式實現相對簡單,主要原理是通過將外掛程式apk和主程式共用一個UserId,主程式通過createPackageContext構造外掛程式的context,通過context即可訪問外掛程式apk中的資源,很多app的主題架構就是通過安裝外掛程式apk的形式實現,例如Go主題。這種方式的缺點就是需要使用者手動安裝,體驗並不是很好。

    不安裝apk的方式解決了使用者手動安裝的缺點,但實現起來比較複雜,主要通過DexClassloader的方式實現,同時要解決如何啟動外掛程式中Activity等Android系統組件,為了保證外掛程式架構的靈活性,這些系統組件不太好在主程式中提前聲明,實現外掛程式架構真正的痛點在此。

    DexClassloader

    這裡引用《深入理解Java虛擬機器:JVM進階特性與最佳實務》第二版裡對java類載入器的一段描述:

    虛擬機器設計團隊把類載入階段中的“通過一個類的全限定名來擷取描述此類的二進位位元組流”這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去擷取所需要的類。實現這個動作的代碼模組稱為“類載入器”。

    Android虛擬機器的實現參考了java的JVM,因此在Android中載入類也用到了類載入器的概念,只是相對於JVM中載入器載入class檔案而言,Android的Dalvik虛擬機器載入的是Dex格式,而具體完成Dex載入的主要是PathClassloaderDexclassloader

    PathClassloader預設會讀取/data/dalvik-cache中緩衝的dex檔案,未安裝的apk如果用PathClassloader來載入,那麼在/data/dalvik-cache目錄下找不到對應的dex,因此會拋出ClassNotFoundException

    DexClassloader可以載入任意路徑下包含dex和apk檔案,通過指定odex產生的路徑,可載入未安裝的apk檔案。下面一段代碼展示了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解決了類的載入問題,如果外掛程式apk裡只是一些簡單的API調用,那麼上面的代碼已經能滿足需求,不過這裡討論的外掛程式架構還需要解決資源訪問和Android系統組件的調用。

    外掛程式內系統組件的調用

    Android Framework中包含ActivityServiceContent Provider以及BroadcastReceiver等四大系統組件,這裡主要討論如何在主程式中啟動外掛程式中的Activity,其它3種組件的調用方式類似。

    大家都知道Activity需要在AndroidManifest.xml中進行聲明,apk在安裝的時候PackageManagerService會解析apk中的AndroidManifest.xml檔案,這時候就決定了程式包含的哪些Activity,啟動未聲明的Activity會報ActivityNotFound異常,相信大部分Android開發人員曾經都遇到過這個異常。

    啟動外掛程式裡的Activity必然會面對如何在主程式中的AndroidManifest.xml中聲明這個Activity,然而為了保證外掛程式架構的靈活性,我們是無法預知外掛程式中有哪些Activity,所以也無法提前聲明。

    為瞭解決上述問題,這裡介紹一種基於Proxy思想的解決方案,大致原理是在主程式的AndroidManifest.xml中聲明一些ProxyActivity,啟動外掛程式中的Activity會轉為啟動主程式中的一個ProxyActivityProxyActivity中所有系統回調都會調用外掛程式Activity中對應的實現,最後的效果就是啟動的這個Activity實際上是主程式中已經聲明的一個Activity,但是相關代碼執行的卻是外掛程式Activity中的代碼。這就解決了外掛程式Activity未聲明情況下無法啟動的問題,從上層來看啟動的就是外掛程式中的Activity。下面具體分析整個過程。

    PluginSDK

    所有的外掛程式和主程式需要依賴PluginSDK進行開發,所有外掛程式中的Activity繼承自PluginSDK中的BasePluginActivityBasePluginActivity繼承自Activity並實現了IPluginActivity介面。

    public interface IPluginActivity {    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 BasePluginActivity extends Activity implements IPluginActivity {    ...    private ClassLoader mDexClassLoader;    private Activity mActivity;    ...        @Override    public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo) {        mIsRunInPlugin = true;        mDexClassLoader = classLoader;        mOutActivity = context;        mApkFilePath = path;        mPackageInfo = packageInfo;        mContext = new PluginContext(context, 0, mApkFilePath, mDexClassLoader);        attachBaseContext(mContext);    }        @Override    protected void onCreate(Bundle savedInstanceState) {        if (mIsRunInPlugin) {            mActivity = mOutActivity;        } else {            super.onCreate(savedInstanceState);            mActivity = this;        }    }    @Override    public void setContentView(int layoutResID) {        if (mIsRunInPlugin) {            mContentView = LayoutInflater.from(mContext).inflate(layoutResID, null);            mActivity.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 PluginProxyActivity extends Activity {    IPluginActivity mPluginActivity;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        Bundle bundle = getIntent().getExtras();        if(bundle == null){            return;        }        mPluginName = bundle.getString(PluginStatic.PARAM_PLUGIN_NAME);        mLaunchActivity = bundle.getString(PluginStatic.PARAM_LAUNCH_ACTIVITY);        File pluginFile = PluginUtils.getInstallPath(PluginProxyActivity.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;        try {            PackageManager pm = getPackageManager();            packageInfo = pm.getPackageArchiveInfo(mPluginApkFilePath, PackageManager.GET_ACTIVITIES);        } catch (Exception e) {            throw e;        }        if (mLaunchActivity == null || mLaunchActivity.length() == 0) {            mLaunchActivity = packageInfo.activities[0].name;        }//        String optimizedDexOutputPath = getDir(odex, Context.MODE_PRIVATE).getAbsolutePath();        ClassLoader classLoader = PluginStatic.getOrCreateClassLoaderByPath(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 = (Class) classLoader.loadClass(mLaunchActivity);        getIntent().setExtrasClassLoader(classLoader);        mPluginActivity = (IPluginActivity) mClassLaunchActivity.newInstance();        mPluginActivity.IInit(mPluginApkFilePath, this, classLoader, packageInfo);    }        ...        @Override    public void startActivityForResult(Intent intent, int requestCode) {        boolean pluginActivity = intent.getBooleanExtra(PluginStatic.PARAM_IS_IN_PLUGIN, false);        if (pluginActivity) {            String launchActivity = null;            ComponentName componentName = intent.getComponent();            if(null != componentName) {                launchActivity = componentName.getClassName();            }            intent.putExtra(PluginStatic.PARAM_IS_IN_PLUGIN, false);            if (launchActivity != null && launchActivity.length() > 0) {                Intent pluginIntent = new Intent(this, getProxyActivity(launchActivity));                pluginIntent.putExtra(PluginStatic.PARAM_PLUGIN_NAME, mPluginName);                pluginIntent.putExtra(PluginStatic.PARAM_PLUGIN_PATH, mPluginApkFilePath);                pluginIntent.putExtra(PluginStatic.PARAM_LAUNCH_ACTIVITY, launchActivity);                startActivityForResult(pluginIntent, requestCode);            }        } else {            super.startActivityForResult(intent, requestCode);        }    }}

    BasePluginActivityPluginProxyActivity在整個外掛程式架構的核心,下面簡單分析一下代碼:

    首先看一下PluginProxyActivity#onResume

    @Overrideprotected void onResume() {    super.onResume();    if(mPluginActivity != null){        mPluginActivity.IOnResume();    }}

    變數mPluginActivity的類型是IPluginActivity,由於外掛程式Activity實現了IPluginActivity介面,因此可以猜測mPluginActivity.IOnResume()最終執行的是外掛程式Activity的onResume中的代碼,下面我們來證實這種猜測。

    BasePluginActivity實現了IPluginActivity介面,那麼這些介面具體是怎麼實現的呢?看代碼:

    @Overridepublic void IOnCreate(Bundle savedInstanceState) {    onCreate(savedInstanceState);}@Overridepublic void IOnResume() {    onResume();}@Overridepublic void IOnStart() {    onStart();}@Overridepublic void IOnPause() {    onPause();}...

    介面實現非常簡單,只是調用了和介面對應的回呼函數,那這裡的回呼函數最終會調到哪裡呢?前面提到過所有外掛程式Activity都會繼承自BasePluginActivity,也就是說這裡的回呼函數最終會調到外掛程式Activity中對應的回調,比如IOnResume執行的是外掛程式Activity中的onResume中的代碼,這也證實了之前的猜測。

    上面的一些程式碼片段揭示了外掛程式架構的核心邏輯,其它的代碼更多的是為實現這種邏輯服務的,後面會提供整個工程的源碼,大家可自行分析理解。

    外掛程式內資源擷取

    實現載入外掛程式apk中的資源的一種思路是將外掛程式apk的路徑加入主程式資源尋找的路徑中,下面的代碼展示了這種方法:

    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訪問資源時使用我們自訂的Context,我們需要在BasePluginActivity的初始化中做一些處理:

    public void IInit(String path, Activity context, ClassLoader classLoader, PackageInfo packageInfo) {    mIsRunInPlugin = true;    mDexClassLoader = classLoader;    mOutActivity = context;    mApkFilePath = path;    mPackageInfo = packageInfo;    mContext = new PluginContext(context, 0, mApkFilePath, mDexClassLoader);    attachBaseContext(mContext);}

    PluginContext中通過重載getAssets來實現包含外掛程式apk尋找路徑的Context:

    public PluginContext(Context base, int themeres, String apkPath, ClassLoader classLoader) {    super(base, themeres);    mClassLoader = classLoader;    mAsset = getSelfAssets(apkPath);    mResources = getSelfRes(base, mAsset);    mTheme = getSelfTheme(mResources);    mOutContext = base;}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;}private Resources getSelfRes(Context ctx, AssetManager selfAsset)   {    DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();    Configuration con = ctx.getResources().getConfiguration();    return new Resources(selfAsset, metrics, con);}private Theme getSelfTheme(Resources selfResources) {    Theme theme = selfResources.newTheme();    mThemeResId = getInnerRIdValue(com.android.internal.R.style.Theme);    theme.applyStyle(mThemeResId, true);    return theme;}@Overridepublic Resources getResources() {    return mResources;}@Overridepublic AssetManager getAssets() {    return mAsset;}...
    總結

    本文描述了一種基於Proxy思想的外掛程式架構,所有的代碼都在Github中,代碼只是抽取了整個架構的核心部分,如果要用在生產環境中還需要完善,比如Content ProviderBroadcastReceiver組件的Proxy類未實現,Activity的Proxy實現也是不完整的,包括不少回調都沒有處理。同時我也無法保證這套架構沒有致命缺陷,本文主要是以總結、學習為目的,歡迎大家一起交流。

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.