插件化,热更新
插件化
App 的部分功能模块在打包时并不以传统方式打包进 apk 文件中,而是以另⼀种形式⼆次封装进 apk内部,或者放在网络上适时下载,在需要的时候动态对这些功能模块进行加载,称之为插件化。这些单独⼆次封装的功能模块 apk ,就称作「插件」,初始安装的 apk 称作「宿主」。插件化是组件化的更进⼀步推进。
插件化步骤:
主Apk分析:
主App打包完成后,会形成dex,image,xml资源
Dex靠PathClassLoader加载
图片以及xml资源靠Resource加载
代码实现:
创建DexClassLoader加载插件代码
创建Resource加载资源文件
管理插件Activity生命周期
插件化基础:反射
使用反射的目的:
Java 既然提供了可见性关键字 public private 等等,用来限制代码之间的可见性。可见性特性的支持不是为了代码不被坏⼈使用,而是是为了程序开发的简洁性。可见性的支持提供的是 Safety 的安全,而不是 Security 的安全。即,可见性的支持让程序更不容易写出 bug,而不是更不容易被人入侵。反射的支持可以让开发者在可见性的例外场景中,可以突破可见性限制来调⽤自己需要的 API。这是基于对开发者「在使⽤反射时已经足够了解和谨慎」的假设的。所以,可见性的支持不是为了防御外来者⼊侵,因此反射功能的⽀持并没有什么不合理。
总结一句话:反射可以让程序员抛开代码的限制,任意使用各种方法。
、、、
try{Class clz=Class.forName("com.king.reflect.Apple");Constructorconstructor=clz.getDeclaredConstructors()[0];constructor.setAccessible(true);Object object=constructor.newInstance();Method method=clz.getDeclaredMethod("setPrice",int.class);method.setAccessible(true);method.invoke(object,4);}catch(IllegalAccessException e){e.printStackTrace();}catch(InstantiationException e){e.printStackTrace();}catch(NoSuchMethodException e){e.printStackTrace();}catch(InvocationTargetException e){e.printStackTrace();}catch(ClassNotFoundException e){e.printStackTrace();}
、、、
插件化原理:动态加载
通过自定义 ClassLoader 来加载新的 dex 文件,从而让程序员原本没有的类可以被使用,这就是插件化的原理。
关于 DEX:
class: java编译后的文件,每个类对应⼀个class文件
dex: Dalvik EXecutable 把class打包在⼀起,⼀个dex可以包含多个class文件
odex: Optimized DEX针对系统的优化,例如某个方法的调用指令,会把虚拟的调用转换为使用具体的 index,这样在执行的时候就不用再查找了
oat: Optimized Android file Type。使用AOT策略对dex预先编译(解释)成本地指令,这样再运行阶段就不需再经历⼀次解释过程,程序的运行可以更快
AOT: Ahead-Of-Time compilation预先编译
Filedexfile=newFile(getCacheDir()+"/plugin.dex");if(!dexfile.exists()){try{InputStreamis=getAssets().open("apk/dex/plugin.dex");intsize=is.available();byte[]buffer=newbyte[size];is.read(buffer);is.close();FileOutputStreamfos=newFileOutputStream(dexfile);fos.write(buffer);fos.close();}catch(Exceptione){thrownewRuntimeException(e);}}try{DexClassLoaderclassLoader=newDexClassLoader(dexfile.getPath(),getCacheDir().getPath(),null,null);ClasspluginUtilsClass=classLoader.loadClass("com.king.reflect.Apple");ConstructorutilsConstructor=pluginUtilsClass.getDeclaredConstructors()[0];Objectutils=utilsConstructor.newInstance();MethodshoutMethod=pluginUtilsClass.getDeclaredMethod("sell");shoutMethod.invoke(utils);}catch(ClassNotFoundExceptione){e.printStackTrace();}catch(IllegalAccessExceptione){e.printStackTrace();}catch(InstantiationExceptione){e.printStackTrace();}catch(NoSuchMethodExceptione){e.printStackTrace();}catch(InvocationTargetExceptione){e.printStackTrace();}}
DexClassLoader构造方法参数说明:
dexPath:apk/dex/jar文件路径
optimizedDirectory:文件解压路径(这个路径下保存的是.dex文件不是.class)
libraryPath:加载时用到的so库(可能理解有问题)
parent:父加载器(这个比较重要与Android加载class的机制有关)
publicDexClassLoader(String dexPath,String optimizedDirectory,String libraryPath,ClassLoader parent){super(dexPath,newFile(optimizedDirectory),libraryPath,parent);}
需要注册的组件(例如 Activity)如何打开
解决方式⼀:代理 Activity
解决方式⼆:欺骗系统
解决方式三:重写 gradle 打包过程,合并 AndroiManifest.xml
我也只是稍微了解方式一的方法。首先在宿主apk中留一个注册的ProxyActivity,生命周期的方法由RealActivity来实现,RealActivity通过插件化的反射来实现。
publicclassProxyActivityextendsActivity{ObjectrealActivity;@OverrideprotectedvoidonCreate(@NullableBundlesavedInstanceState){super.onCreate(savedInstanceState);realActivity.onCreate(savedInstanceState);}.....}
资源文件如何加载
解决方式:自定义 AssetManager 和 Resources 对象,重写getResources() 和 getAssets() 方法。
```
publicclassProxyActivityextendsActivity{@OverridepublicAssetManagergetAssets(){try{ClassassetManagerClass=AssetManager.class;AssetManagerassetManager=(AssetManager)assetManagerClass.newInstance();MethodaddAssetPath=assetManagerClass.getDeclaredMethod("addAssetPath",String.class);addAssetPath.invoke(assetManager,"apkPath");returnassetManager;}catch(IllegalAccessExceptione){e.printStackTrace();}catch(InstantiationExceptione){e.printStackTrace();}catch(NoSuchMethodExceptione){e.printStackTrace();}catch(InvocationTargetExceptione){e.printStackTrace();}returnsuper.getAssets();}@OverridepublicResourcesgetResources(){returnnewResources(getAssets(),getResources().getDisplayMetrics(),getResources().getConfiguration());}}
```
插件化的作用
解决 dex 65535 问题。现在由 multidex 工具来专门解决。
减小安装包大小。
动态部署。
热更新
不安装新版本的软件,直接从网络下载新功能模块来对软件进行局部更新
热更新和插件化的区别
插件化的内容在原 App 中没有,而热更新是原 App 中的内容做了改动
插件化在代码中有固定的入口,而热更新则可能改变任何⼀个位置的代码
热更新的原理
ClassLoader 的 dex 文件替换
直接修改字节码
loadClass() 的类加载过程(双亲委托):是⼀个带缓存的、从上到下的加载过程(如下图所示):
灰色部分为缓存:
双亲委托.png
对于具体的⼀个 ClassLoader:
先从自己的缓存中取
自己没有缓存,就找父ClassLoader 要(parent.loadClass())
父ClassLoader也没有,就自己加载(findClass())BaseDexClassLoader 或者它的⼦类(DexClassLoader、 PathClassLoader 等)的findClass():通过 pathList.findClass(),它的 pathList.loadClass() 通过 DexPathList 的 dexElements 的 findClass()。
```
publicClass<?>loadClass(Stringname)throwsClassNotFoundException{returnloadClass(name,false);}// protectedClass<?>loadClass(Stringname,boolean resolve)throwsClassNotFoundException{// First, check if the class has already been loadedClass<?>c=findLoadedClass(name);if(c==null){try{if(parent!=null){c=parent.loadClass(name,false);}else{c=findBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if(c==null){// If still not found, then invoke findClass in order// to find the class.c=findClass(name);}}returnc;}
```
所以热更新的关键在于,把补丁 dex 文件加载放进⼀个 Element,并且插入到 dexElements 这个数组的前面(插入到后面的话会被忽略掉)
热更新代码
因为无法在更新之前就指定要更新谁;所以不能定义新的 ClassLoader,而只能选择对ClassLoader 进行修改,让它能够加载补丁里面的类。因为补丁的类在原先的 App 中已经存在,所以应该把补丁的 Element 对象插入到 dexElements 的前面才行,插入到后面会被忽略掉。
具体的代码:
自己用补丁创建⼀个 PathClassLoader
把补丁 PathClassLoader 里面的 elements 替换到就的里面去, 尽早加载热更新(通用手段是把加载过程放在 Application.attachBaseContext())
```
try{ClassLoaderclassLoader=getClassLoader();ClassloaderClass=BaseDexClassLoader.class;FieldpathListField=loaderClass.getDeclaredField("pathList");pathListField.setAccessible(true);ObjectpathListObject=pathListField.get(classLoader);ClasspathListClass=pathListObject.getClass();FielddexElementsField=pathListClass.getDeclaredField("dexElements");dexElementsField.setAccessible(true);ObjectdexElementsObject=dexElementsField.get(pathListObject);// classLoader.pathList.dexElements = 热更新内容elements ;PathClassLoadernewClassLoader=newPathClassLoader(apk.getPath(),null);ObjectnewPathListObject=pathListField.get(newClassLoader);ObjectnewDexElementsObject=dexElementsField.get(newPathListObject);//将newElement添加到Element[]的前面intoldLength=Array.getLength(dexElementsObject);intnewLength=Array.getLength(newDexElementsObject);ObjectconcatDexElementsObject=Array.newInstance(dexElementsObject.getClass().getComponentType(),oldLength+newLength);for(inti=0;i<newLength;i++){Array.set(concatDexElementsObject,i,Array.get(newDexElementsObject,i));}for(inti=0;i<oldLength;i++){Array.set(concatDexElementsObject,newLength+i,Array.get(dexElementsObject,i));}dexElementsField.set(pathListObject,concatDexElementsObject);}catch(NoSuchFieldExceptione){e.printStackTrace();}catch(IllegalAccessExceptione){e.printStackTrace();}
```