插件化,热更新

2019-12-04  本文已影响0人  執O_o念

插件化

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();}

```

上一篇下一篇

猜你喜欢

热点阅读