Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 基於Proxy思想的Android插件框架

基於Proxy思想的Android插件框架

編輯:關於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

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

    變量mPluginActivity的類型是IPluginActivity,由於插件Activity實現了IPluginActivity接口,因此可以猜測mPluginActivity.IOnResume()最終執行的是插件Activity的onResume中的代碼,下面我們來證實這種猜測。

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

    @Override
    public void IOnCreate(Bundle savedInstanceState) {
        onCreate(savedInstanceState);
    }
    
    @Override
    public void IOnResume() {
        onResume();
    }
    
    @Override
    public void IOnStart() {
        onStart();
    }
    
    @Override
    public 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;
    }
    
    @Override
    public Resources getResources() {
        return mResources;
    }
    
    @Override
    public AssetManager getAssets() {
        return mAsset;
    }
    
    ...

    總結

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

    1. 上一頁:
    2. 下一頁:
    熱門文章
    閱讀排行版
    Copyright © Android教程網 All Rights Reserved