android插件化资源加载

北大青鸟大学城校区logo 北大青鸟大学城校区
招生简章校园环境师资力量就业明星招生问答软件工程师北京大学学历学员项目联系我们 报名通道

免费在线咨询通道>>

免费在线报名通道>>

北大青鸟报名电话
当前位置:北大青鸟 > IT培训 > android培训 >

android插件化资源加载

标签:   分类:android培训

android插件化探索(一)类加载器DexClassLoader。

PathClassLoader和DexClassLoader的区别

  DexClassLoader的源码如下:
publicclassDexClassLoaderextendsBaseDexClassLoader{//支持从任何地方的apk/jar/dex中读取publicDexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {super(dexPath,newfile(optimizedDirectory), libraryPath, parent); } }
 
  PathClassLoader的源码如下,没有指定optimizedDirectory所以只能加载已安装的APK,因为已安装的APK会将dex解压到了/data/dalvik-cache/目录下,PathClassLoader会到这里去找。

  publicclassPathClassLoaderextendsBaseDexClassLoader{publicPathClassLoader(String dexPath, ClassLoader parent) {super(dexPath,nullnull, parent); }publicPathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {super(dexPath,null, libraryPath, parent); } }
  但是本人本着不作不死的性格,修改Plugin类如下:

  privatevoiduseDexClassLoader(string path){//创建类加载器,把dex加载到虚拟机中PathClassLoader calssLoader =newPathClassLoader(path,nullthis.getClass.getClassLoader);//利用反射调用插件包内的类的方法try{ Class<?> clazz = calssLoader.loadClass("com.maplejaw.hotplugin.PluginClass"); Commobj = (Comm)clazz.newInstance; Integer ret= obj.function(1221); Log.d("JG""返回的调用结果: "+ ret); }catch(Exception e){ e.printStackTrace; } }
  在4.4和5.0上分别做了试验从SD卡上加载dex,发现提示略有差别。

  Android 4.4:直接提示找不到该类

  java.lang.ClassNotFoundException: Didn't find class "com.maplejaw.hotplugin.PluginClass" on path: DexPathList[[zip file "/storage/sdcard/2.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]Android 5.0:可以发现在加载该类之前,系统尝试将dex写到/data/dalvik-cache/下,由于权限问题而失败。

  05-2504:07:02.29131699-31699/com.maplejaw.hotfix E/dalvikvm: Dex cache directory isn't writable: /data/dalvik-cache 05-25 04:07:02.291 31699-31699/com.maplejaw.hotfix I/dalvikvm: Unable to open or create cache for /storage/sdcard/2.apk (/data/dalvik-cache/storage@sdcard@2.apk@classes.dex) 05-25 04:07:02.291 31699-31699/com.maplejaw.hotfix W/System.err: java.lang.ClassNotFoundException: Didn't find class"com.maplejaw.hotplugin.PluginClass"on path: DexPathList[[zip file"/storage/sdcard/2.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]

  但是!!!笔者此时默默的掏出了大红米(Android 5.0)测试了一番。居然发现可以调用正常,也就是说,MIUI成功将dex写到了/data/dalvik-cache/下,所以如果你手持MIUI发现PathClassLoader可以加载外部dex时,务必冷静,用模拟器试试。

  双亲委托

  为 了更好的保证 JAVA 平台的安全。在此模型下,当一个装载器被请求加载某个类时,先委托自己的 parent 去装载,如果 parent 能装载,则返回这个类对应的 Class 对象,否则,递归委托给父类的父类装载。当所有父类装载器都装载失败时,才由当前装载器装载。在此模型下,用户自定义的类装载器,不可能装载应该由父亲装 载的可靠类,从而防止不可靠甚至恶意的代码代替本应该由父亲装载器装载的可靠代码。
  在jvm中预定义了的三种类型类加载器:

  启动(bootstrap)类加载器:是用本地代码实现的类装入器,它负责将 /lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
  委派机制:DexClassLoader->PathClassLoader->BootClassLoader。
  我们可以打印出其委派机制:

  ClassLoader classLoader=newDexClassLoader(apkPath,getApplicationInfo.dataDir,libPath,getClassLoader);try{ Class<?> clazz = calssLoader.loadClass("com.maplejaw.hotplugin.PluginClass"); Comm obj = (Comm)clazz.newInstance; Integer ret = obj.function(1221);while(classLoader !=null){ Log.d("JG""类加载器:"+classLoader); classLoader = classLoader.getParent; } Log.d("JG""返 回的调用结果: "+ ret);
  结果如下:

  05-2509:09:11.3309100-9100/com.maplejaw.hotfix D/JG: 初始化PluginClass05-2509:09:11.3309100-9100/com.maplejaw.hotfix D/JG: 类加载器:dalvik.system.DexClassLoader[DexPathList[[zip file"/data/app/com.maplejaw.hotplugin-2/base.apk"],nativeLibraryDirectories=[/data/app/com.maplejaw.hotplugin-2/lib/x86_64, /vendor/lib64, /system/lib64]]]05-2509:09:11.3309100-9100/com.maplejaw.hotfix D/JG: 类加载器:dalvik.system.PathClassLoader[DexPathList[[zip file"/data/app/com.maplejaw.hotfix-2/base.apk"],nativeLibraryDirectories=[/data/app/com.maplejaw.hotfix-205-2509:09:11.3309100-9100/com.maplejaw.hotfix D/JG: 类加载器:java.lang.BootClassLoader@7cd98df05-2509:09:11.3309100-9100/com.maplejaw.hotfix D/JG: 返回的调用结果:33看到这里,应该可以明白上一篇中提到的java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation中因为同一加载器加载不同dex中相同的类引发的错误了吧。

  那么就来看看LoadedApk的getResources源码。

  publicResourcesgetResources(ActivityThread mainThread) {if(mResources ==null) { mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs, mApplicationInfo.sharedLibraryfiles, Display.DEFAULT_DISPLAY,nullthis); }returnmResources; }

  可以看出,LoadedApk会调用ActivityThread去加载。最终在ResourcesManager中找到了真身。

  Resources getTopLevelResources(String resDir, String splitResDirs, String overlayDirs, String libDirs,intdisplayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo) { Resources r;//...//省略了部分源码AssetManager assets =newAssetManager;if(resDir !=null) {if(assets.addAssetPath(resDir) ==0) {returnnull; } }if(splitResDirs !=null) {for(String splitResDir : splitResDirs) {if(assets.addAssetPath(splitResDir) ==0) {returnnull; } } }if(overlayDirs !=null) {for(String idmapPath : overlayDirs) { assets.addOverlayPath(idmapPath); } }if(libDirs !=null) {for(String libDir : libDirs) {if(libDir.endsWith(".apk")) {if(assets.addAssetPath(libDir) ==0) { Log.w(TAG,"Asset path '"+ libDir +"' does not exist or contains no resources."); } } } }//...//省略了部分源码r =newResources(assets, dm, config, compatInfo);returnr; } }代码有点长,但是很显然,最终的资源加载交给了

  AssetManager,assets.addAssetPath(libDir)添加资源目录,然后new了一个Resources对象返回。
那我们现在通过反射来模仿系统的写法。

  protectedvoidloadResources(String dexPath) {try{ AssetManager assetManager = AssetManager.class.newInstance; Method addAssetPath = assetManager.getClass.getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, dexPath); Resources superRes =super.getResources; mResources =newResources(assetManager, superRes.getDisplayMetrics,superRes.getConfiguration); }catch(Exception e) { e.printStackTrace; } }
然后重写getResources方法:

  @OverridepublicResourcesgetResources {returnmResources ==null?super.getResources : mResources; }
核心代码修改如下,
//先加载插件资源loadResources(apkPath);//核心代码调用Class<?> clazz = classLoader.loadClass(packageName+".PluginClass"); Comm obj = (Comm) clazz.newInstance; mImageView.setImageDrawable(obj.getDrawable(getgetResources));
成功将资源加载到宿主apk,测试通过。

  当然,为了使通用性更强,不因主题的差异而导致效果不一样,上面的代码一般会这么写
//反射加载资源privateAssetManager mAssetManager;privateResources mResources;privateResources.Theme mTheme;protectedvoidloadResources(String dexPath) {try{ AssetManager assetManager = AssetManager.class.newInstance; Method addAssetPath = assetManager.getClass.getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, dexPath); mAssetManager = assetManager; }catch(exception e) { e.printStackTrace; } Resources superRes =super.getResources; mResources =newResources(mAssetManager, superRes.getDisplayMetrics,superRes.getConfiguration); mTheme = mResources.newTheme; mTheme.setTo(super.getTheme); }//重写方法@OverridepublicAssetManagergetAssets {returnmAssetManager ==null?super.getAssets : mAssetManager; }@OverridepublicResourcesgetResources {returnmResources ==null?super.getResources : mResources; }@OverridepublicResources.ThemegetTheme {returnmTheme ==null?super.getTheme : mTheme; }换皮肤原理
换皮肤一般有两种方式。

  约定好资源名字

这种方式非常简单,基本不需要修改什么代码。只需两部就来完成。
假如我们有一个图片菜单,在宿主和插件中都叫a.png。
设置菜单图片的代码如下。

  mImageMenu.setImageDrawable(getResources.getDrawable(R.drawable.a));
              重写getResources
              获取Resources对象
  loadResources或getResourcesForapplication
  这样在需要加载皮肤的地方loadResources,然后重写加载就能实现换肤功能。

  不约定资源名字

  这种方式主要通过接口的方式进行调用。让不同的皮肤插件进行调用。
            实现插件接口
  publicDrawablegetImageMenu(resources res){returnres.getDrawable(R.drawable.a); }
            重写getResources
            获取Resources对象
            设置菜单图片
  Class<?> clazz = classLoader.loadClass(packageName+".PluginClass"); Comm obj = (Comm) clazz.newInstance; mImageMenu.setImageDrawable(obj.getImageMenu(getResources));
从上面可以看出,约定资源名字这种方法,可以少写好多接口。
  学android,就来北大青鸟大学城校区,高薪好工作!
 

若有疑问请拨打北大青鸟咨询热线:010-80146691或点击免费在线咨询!
  • xml地图 网站地图 招生简章 合作企业 学员项目 联系我们
  • 关闭窗口