Android插件化開發之動態加載技術學習

爲何要插件化開發和動態加載呢?我認爲緣由有三點:

  • 能夠實現解耦
  • 能夠解除單個dex函數不能超過65535的限制
  • 能夠給apk瘦身,好比說360安全衛士,整個安裝包才13.7M,對於一個用戶量上億的app這個大小已經很小了,它裏面不少功能都是以插件的形式存在的

插件化開源框架有哪些

目前插件化開發已不是什麼高深的技術了,各大互聯網公司基本都有本身插件化開發框架,並且大部分都已經開源出來,聽起來都是很高大上的東西,可是他們的原理有沒有真正瞭解過呢?這兩天經過查找的一些資料,想跟你們分享一下。

主要解決三個問題

  1. 如何加載插件apk的資源文件?
  2. 如何調用插件apk的方法?
  3. 如何加載插件中的activity,而且有生命週期?

第一個問題:如何加載插件apk的資源文件?

對於第一個問題咱們假設有這麼一個需求:咱們有個app想作相似qq換膚的功能,可是這個皮膚文件很大,若是跟宿主app一塊兒打包的話可能會致使apk包很大,但願經過插件的方式,在用戶須要換膚的時候去下載各類皮膚插件,來完成換膚的需求。android

首先要了解一個類:git

  • DexClassLoader
DexClassLoader是一個類加載器,能夠用來從.jar和.apk文件中加載class。能夠用來加載執行沒用和應用程序一塊兒安裝的那部分代碼。
構造函數:
DexClassLoader(
String dexPath, //被解壓的apk路徑,不能爲空。
String optimizedDirectory, //解壓後的.dex文件的存儲路徑,不能爲空。這個路徑強烈建議使用應用程序的私有路徑,不要放到sdcard上,不然代碼容易被注入攻擊。
String libraryPath, //os庫的存放路徑,能夠爲空,如有os庫,必須填寫。
ClassLoader parent//父親加載器,通常爲ClassLoader.getSystemClassLoader()。
)
  • AssetManager
    中的內部的方法addAssetPath,
    將插件apk路徑傳入,從而添加進assetManager中,
    而後經過new Resource把assetManager傳入構造方法中,
    能夠獲得未安裝apk對應的Resource對象。
/**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public final int addAssetPath(String path) {
        int res = addAssetPathNative(path);
        return res;
    }

接下來解決這個問題的思路是,先把插件apk下載到本地sd卡上,而後獲取這個apk的信息,最後用DexClassLoader動態加載github

第一步,下載插件apk:數組

/**
     * 下載插件apk
     * */
    private void downLoadPlugApk() {
        DownloadUtils.get().downloadFile(APK_URL, new File(PLUG_APP_PATH, APK_NAME), new DownLoadListener() {
            @Override
            public void onFail(File file) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(UnInstallActivity.this,"下載失敗",Toast.LENGTH_LONG).show();
                    }
                });

            }

            @Override
            public void onSucess(File file) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        btn_download_plug_apk.setText("下載插件apk");
                        Toast.makeText(UnInstallActivity.this,"下載成功",Toast.LENGTH_LONG).show();
                    }
                });

            }

            @Override
            public void onProgress(long bytesRead, long contentLength, boolean done) {
                LogUtils.d("contentLength:"+contentLength+" | bytesRead:"+bytesRead+" | done:"+done);
                final float persent = (float) bytesRead / contentLength*100;
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        btn_download_plug_apk.setText((int)persent+"%");
                    }
                });
            }
        });

    }

這個插件apk裏面有一張圖片test.png放在mipmap-xxhdpi目錄下,我是先把plugapp.apk文件放在一個服務器上,經過代碼下載到sd卡的根目錄下面安全

第二步,獲取plugapk的信息 經過PackageManager的getPackageArchiveInfo方法得到服務器

/**
     * 獲取未安裝apk的信息
     * @param context
     * @param apkPath apk文件的path
     * @return
     */
    private String[] getUninstallApkInfo(Context context, String apkPath) {
        String[] info = new String[2];
        PackageManager pm = context.getPackageManager();
        PackageInfo pkgInfo = pm.getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);
        if (pkgInfo != null) {
            ApplicationInfo appInfo = pkgInfo.applicationInfo;
            String versionName = pkgInfo.versionName;//版本號
            Drawable icon = pm.getApplicationIcon(appInfo);//圖標
            String appName = pm.getApplicationLabel(appInfo).toString();//app名稱
            String pkgName = appInfo.packageName;//包名
            info[0] = appName;
            info[1] = pkgName;
        }
        return info;
    }

第三步,獲取Resource對象cookie

/**
     * @param apkPath
     * @return 獲得對應插件的Resource對象
     * 經過獲得AssetManager中的內部的方法addAssetPath,
     * 將未安裝的apk路徑傳入從而添加進assetManager中,
     * 而後經過new Resource把assetManager傳入構造方法中,進而獲得未安裝apk對應的Resource對象。
     */
    private Resources getPluginResources(String apkPath) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);//反射調用方法addAssetPath(String path)
            //第二個參數是apk的路徑:Environment.getExternalStorageDirectory().getPath()+File.separator+"plugin"+File.separator+"apkplugin.apk"
            //將未安裝的Apk文件的添加進AssetManager中,第二個參數爲apk文件的路徑帶apk名
            addAssetPath.invoke(assetManager, apkPath);
            Resources superRes = this.getResources();
            Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
            return mResources;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

第四步,經過DexClassLoader得到resid數據結構

/**
     * 加載apk得到內部資源
     * @param apkPath apk路徑
     * @throws Exception
     */
    private int getRecourceIdFromPlugApk(String apkPath,String apkPackageName) throws Exception {
        File optimizedDirectoryFile = getDir("dex", Context.MODE_PRIVATE);//在應用安裝目錄下建立一個名爲app_dex文件夾目錄,若是已經存在則不建立
        Log.v("zxy", optimizedDirectoryFile.getPath().toString());// /data/data/com.example.dynamicloadapk/app_dex
        //參數:一、包含dex的apk文件或jar文件的路徑,二、apk、jar解壓縮生成dex存儲的目錄,三、本地library庫目錄,通常爲null,四、父ClassLoader
        DexClassLoader dexClassLoader = new DexClassLoader(apkPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
        Class<?> clazz = dexClassLoader.loadClass(apkPackageName + ".R$mipmap");//經過使用apk本身的類加載器,反射出R類中相應的內部類進而獲取咱們須要的資源id
        Field field = clazz.getDeclaredField("test");//獲得名爲test的這張圖片字段
        int resId = field.getInt(R.id.class);//獲得圖片id
        return resId;
    }

第五步,實現換膚效果app

/**
     * 加載資源
     * */
    private void loadPlugResource() {

        String[] apkInfo = getUninstallApkInfo(this, PLUG_APP_PATH + "/" + APK_NAME);
        String appName = apkInfo[0];
        String pkgName = apkInfo[1];
        Resources resource = getPluginResources(APK_PATH);
        try {
            int resid = getRecourceIdFromPlugApk(APK_PATH, pkgName);
            activity_un_install.setBackgroundDrawable(resource.getDrawable(resid));
        } catch (Exception e) {
            e.printStackTrace();

        }

    }

第二個問題:如何調用插件apk的方法?

根據第一個問題就能夠獲得答案, 經過DexClassLoader加載類,而後經過反射機制執行類裏面的方法框架

/**
     * @param apkPath apk路徑
     * @throws Exception
     */
    private String runPlugApkMethod(String apkPath,String apkPackageName) throws Exception {
        File optimizedDirectoryFile = getDir("dex", Context.MODE_PRIVATE);//在應用安裝目錄下建立一個名爲app_dex文件夾目錄,若是已經存在則不建立
        Log.v("zxy", optimizedDirectoryFile.getPath().toString());// /data/data/com.example.dynamicloadapk/app_dex
        //參數:一、包含dex的apk文件或jar文件的路徑,二、apk、jar解壓縮生成dex存儲的目錄,三、本地library庫目錄,通常爲null,四、父ClassLoader
        DexClassLoader dexClassLoader = new DexClassLoader(apkPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
//        //經過使用apk本身的類加載器,反射出R類中相應的內部類進而獲取咱們須要的資源id
//        Class<?> clazz = dexClassLoader.loadClass(apkPackageName + ".R$mipmap");
//        Field field = clazz.getDeclaredField("test");//獲得名爲test的這張圖片字段
//        int resId = field.getInt(R.id.class);//獲得圖片id

        // 使用DexClassLoader加載類
        Class libProvierClazz = dexClassLoader.loadClass(apkPackageName+".TestDynamic");
        //經過反射運行sayHello方法
        Object obj=libProvierClazz.newInstance();
        Method method=libProvierClazz.getMethod("sayHello");
        return (String)method.invoke(obj);

    }

第三個問題:如何加載插件中的activity,而且有生命週期?

這個問題是最關鍵的問題,咱們知道經過DexClassLoader能夠加載插件app裏的任何類包括Activity,也能夠執行其中的方法,可是Android中的四大組件都有一個特色就是他們有本身的啓動流程和生命週期,咱們使用DexClassLoader加載進來的Activity是不會涉及到任何啓動流程和生命週期的概念,說白了,他就是一個普普統統的類。因此啓動確定會出錯。
這裏就要看一下activity的啓動流程了,步驟太多就不寫了,能夠網上搜一下資料或者看《Android源碼情景分析》這本書介紹的很詳細,一個簡單的啓動要涉及到30多個步驟。
加載Activity的時候,有一個很重要的類:LoadedApk.Javaimage
他內部有一個mClassLoader變量是負責加載一個Apk程序d的,因此能夠從這裏入手,咱們首先要獲取這個對象,這個對象在ActivityThread中有實例,
imageActivityThread類中有一個本身的static對象,而後還有一個ArrayMap存放Apk包名和LoadedApk映射關係的數據結構,那麼咱們分析清楚了,下面就來經過反射來獲取mClassLoader對象。

private void loadApkClassLoader(DexClassLoader dLoader){
        try{
            String filesDir = this.getCacheDir().getAbsolutePath();
            String libPath = filesDir+File.separator+APK_NAME;

            // 配置動態加載環境
            Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {});//獲取主線程對象 http://blog.csdn.net/myarrow/article/details/14223493
            //當前apk的包名
            String packageName = this.getPackageName();
            ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect( "android.app.ActivityThread", currentActivityThread, "mPackages");
            WeakReference wr = (WeakReference) mPackages.get(packageName);
            RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader", wr.get(), dLoader);

        }catch(Exception e){
            e.printStackTrace();
        }


    }

因此咱們是經過將LoadedApk中的mClassLoader替換成咱們的DexClassLoader來實現加載plugappActivity的

/**
     * 運行插件apk
     * */
    private void runPlug() {
        String filesDir = this.getCacheDir().getAbsolutePath();
        String libPath = filesDir+File.separator+APK_NAME;
        loadResources(libPath);
        DexClassLoader loader = new DexClassLoader(libPath, filesDir, filesDir, ClassLoader.getSystemClassLoader());
//        DexClassLoader loader = new DexClassLoader(libPath, filesDir,null, getClassLoader());
        Class<?> clazz = null;
        try {
            clazz = loader.loadClass("com.demo.plug.MainActivity");

            Class rClazz = loader.loadClass("com.demo.plug.R$layout");
            Field field = rClazz.getField("activity_main");
            Integer ojb = (Integer)field.get(null);

            View view = LayoutInflater.from(this).inflate(ojb, null);

            Method method = clazz.getMethod("setLayoutView", View.class);
            method.invoke(null, view);
            Log.i("demo", "field:"+ojb);

            loadApkClassLoader(loader);

            Intent intent = new Intent(RunPlugActivity.this, clazz);
            startActivity(intent);

        } catch (Throwable e) {
            Log.i("inject","error:"+Log.getStackTraceString(e));
            e.printStackTrace();
        }

    }

說白了就是偷樑換柱,欺騙系統來達到啓動插件的目的。360的插件框架就是使用這種技術稱之爲hook技術,而後經過預先佔坑的方式來預註冊Activity。攜程的這套插件化開發框架則是使用代理的模式來實現啓動插件Activity的,全部activity都須要繼承自proxy avtivity(proxy avtivity負責管理全部activity的生命週期),它的優勢是不須要預先佔坑了(不須要預先在宿主的清單文件裏註冊actvity)缺點是不支持Service和BroadCastReceiver,由於activity的生命週期啓動仍是比較複雜的,因此我的以爲攜程的這套插件化框架實現起來是比較有難度的。

最後,除了上面這種方式還有兩種

  • 經過合併PathClassLoader和DexClassLoader中的dexElements數組,
  • 動態代理加載Activity

這裏只是作了一個最簡單的探討,若是想要作一套插件化開發框架可能要對android的framework層有一個更深刻的理解,可是大概原理和思路我以爲是差很少的。
附一個下載連接:http://download.csdn.net/deta...

相關文章
相關標籤/搜索