目前插件化開發已不是什麼高深的技術了,各大互聯網公司基本都有本身插件化開發框架,並且大部分都已經開源出來,聽起來都是很高大上的東西,可是他們的原理有沒有真正瞭解過呢?這兩天經過查找的一些資料,想跟你們分享一下。
對於第一個問題咱們假設有這麼一個需求:咱們有個app想作相似qq換膚的功能,可是這個皮膚文件很大,若是跟宿主app一塊兒打包的話可能會致使apk包很大,但願經過插件的方式,在用戶須要換膚的時候去下載各類皮膚插件,來完成換膚的需求。android
首先要了解一個類:git
DexClassLoader是一個類加載器,能夠用來從.jar和.apk文件中加載class。能夠用來加載執行沒用和應用程序一塊兒安裝的那部分代碼。 構造函數: DexClassLoader( String dexPath, //被解壓的apk路徑,不能爲空。 String optimizedDirectory, //解壓後的.dex文件的存儲路徑,不能爲空。這個路徑強烈建議使用應用程序的私有路徑,不要放到sdcard上,不然代碼容易被注入攻擊。 String libraryPath, //os庫的存放路徑,能夠爲空,如有os庫,必須填寫。 ClassLoader parent//父親加載器,通常爲ClassLoader.getSystemClassLoader()。 )
/** * 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(); } }
根據第一個問題就能夠獲得答案, 經過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); }
這個問題是最關鍵的問題,咱們知道經過DexClassLoader能夠加載插件app裏的任何類包括Activity,也能夠執行其中的方法,可是Android中的四大組件都有一個特色就是他們有本身的啓動流程和生命週期,咱們使用DexClassLoader加載進來的Activity是不會涉及到任何啓動流程和生命週期的概念,說白了,他就是一個普普統統的類。因此啓動確定會出錯。
這裏就要看一下activity的啓動流程了,步驟太多就不寫了,能夠網上搜一下資料或者看《Android源碼情景分析》這本書介紹的很詳細,一個簡單的啓動要涉及到30多個步驟。
加載Activity的時候,有一個很重要的類:LoadedApk.Java
他內部有一個mClassLoader變量是負責加載一個Apk程序d的,因此能夠從這裏入手,咱們首先要獲取這個對象,這個對象在ActivityThread中有實例,
ActivityThread類中有一個本身的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的生命週期啓動仍是比較複雜的,因此我的以爲攜程的這套插件化框架實現起來是比較有難度的。
這裏只是作了一個最簡單的探討,若是想要作一套插件化開發框架可能要對android的framework層有一個更深刻的理解,可是大概原理和思路我以爲是差很少的。
附一個下載連接:http://download.csdn.net/deta...