目前插件化开发已不是什么高深的技术了,各大互联网公司基本都有本身插件化开发框架,并且大部分都已经开源出来,听起来都是很高大上的东西,可是他们的原理有没有真正了解过呢?这两天经过查找的一些资料,想跟你们分享一下。
对于第一个问题咱们假设有这么一个需求:咱们有个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...