当前位置: 首页 > 科技观察

深入理解Android插件技术原理

时间:2023-03-20 13:08:37 科技观察

前言插件技术最初起源于apk免安装运行的思想。这个没有安装的apk可以理解为插件。支持插件的应用可以在运行时加载和运行插件,从而可以将应用中一些不常用的功能模块做成插件。一方面减小了安装包的大小,另一方面可以实现app功能的动态扩展。;今天我们来说说插件一、插件介绍1、插件介绍在Android系统中,应用程序以Apk的形式存在,所有的应用程序都需要安装后才能使用。但实际上,在Android系统中安装应用程序的方式是相当简单的。其实就是将应用程序Apk复制到系统的不同目录下,然后解压so;常见的应用安装目录有:/system/app:系统应用/system/priv-app:系统应用/data/app:用户应用的Apk组成,一个常见的Apk会包含以下几部分:classes.dex:Java代码bytecoderes:资源目录lib:so目录assets:静态资源目录AndroidManifest.xml:Manifest文件其实Android系统打开应用程序后,只是打开一个进程,然后使用ClassLoader将classes.dex加载到进程中执行相应的组件;那么你可能会想到一个问题,既然Android本身也使用了类似的反射,为什么我们不能执行Apk中的代码呢?这其实就是插件的目的,让Apk(主要指Android组件)中的代码无需安装即可运行,可以带来很多好处。最明显的优势其实就是通过网络进行热更新和热修复;2、插件技术难点反映并执行插件Apk中的代码(ClassLoaderInjection),使系统调用插件Apk中的组件(RuntimeContainer),正确识别插件Apk资源(ResourceInjection)3.双亲委派机制ClassLoader调用loadClass方法加载类,代码如下:classes(className);if(clazz==null){ClassNotFoundExceptionsuppressed=null;try{//如果还没有加载,先调用父加载器的loadClassclazz=parent.loadClass(className,false);}catch(ClassNotFoundExceptione){suppressed=e;}if(clazz==null){try{//父加载器未加载,则尝试加载clazz=findClass(className);}catch(ClassNotFoundExceptione){e.addSuppressed(suppressed);Throwe;}}}returnclazz;}可以看出,ClassLoader在加载一个类时,首先检查自己是否已经加载了该类。如果它还没有加载它,它会先让父加载器加载它。如果父加载器无法加载该类,它会调用自己的findClass方法进行加载,这种机制很大程度上避免了类的重复加载;二、插件详解1.ClassLoaderInjection简单来说,在插件场景下,同一个进程中会有多个ClassLoader场景:HostClassLoader:宿主是安装应用,插件是自动的在运行ClassLoader加载时创建;并且由于ClassLoader的双亲委派机制,实际上系统类不会受到ClassLoader类隔离机制的影响,使得宿主Apk可以在宿主进程中使用来自插件的组件类;2.RuntimeContainerClassLoaderinjection之后就可以在宿主进程中使用插件Apk中的类了,但是我们都知道Android组件是通过系统调用启动的,卸载后的Apk中的组件是没有注册AMS的,PMS,就像你直接在插件Apk中使用startActivity启动一个组件,系统会告诉你找不到;我们的方案很简单,就是运行时容器技术,简单的在宿主Apk中放一些空的Android组件,以Activity为例,我在宿主中预置了一个ContainerActivityextendsActivity,注册到AndroidManifest.xml中;它要做的事情很简单,就是帮助我们作为插件Activity的容器,它从我开始Intent接受几个参数,分别是插件的不同信息,如:pluginName;插件ApkPath;pluginActivityName等,其实最重要的就是pluginApkPath和pluginActivityName。ContainerActivity启动时,我们加载插件的ClassLoader和Resource,并反射对应的pluginActivityNameActivity类;当加载完成后,ContainerActivity会做两件事:将系统所有的生命周期回调转发给插件Activity接受Activity方法的系统调用,并转发回系统我们可以通过重写生命周期来完成第一件事情ContainerActivity的方法第二步,我们需要定义一个PluginActivity,然后在插件Apk中写Activity组件的时候,不再让它集成android.app.Activity,而是从我们的PluginActivity中集成,然后replaceitwithbytecode这个操作是自动完成的,后面会讲为什么。我们先看伪代码;publicclassContainerActivityextendsActivity{privatePluginActivitypluginActivity;@OverrideprotectedvoidonCreate(BundlesssavedInstanceState){StringpluginActivityName=getIntent().getString("pluginActivityName","");pluginActivity(NamepluginActivity=PluginActivityLoadthis);if(pluginActivity==null){super.onCreate(savedInstanceState);返回;}pluginActivity.onCreate();}@OverrideprotectedvoidonResume(){if(pluginActivity==null){super.onResume();return;}pluginActivity.onResume();}@OverrideprotectedvoidonPause(){if(pluginActivity==null){super.onPause();return;}pluginActivity.onPause();}//...}publicclassPluginActivity{privateContainerActivitycontainerActivity;publicPluginActivity(ContainerActivitycontainerActivity){this.containerActivity=containerActivity;}@OverridepublicTfindViewById(intid){returncontainerActivity.findViewById(id);}//...}//实际写在插件中的组件`Apk`publicclassTestActivityextendsPluginActivity{//...}但是总的原理就是这么简单。启动一个插件组件,需要依赖容器。容器负责加载插件组件并完成双向转发,将系统的生命周期回调转发给插件组件,将插件组件的系统调用转发给系统;3.资源注入最后要说的就是资源注入,其实挺重要的。Android应用的开发其实提倡逻辑和资源分离的理念,所有的资源(layout,values等)都会被打包成Apk,然后生成对应的R类,里面包含了所有资源的引用id;资源的注入并不容易,幸好Android系统给我们留了后路,最重要的就是这两个接口:PackageManager#getResourcesForApplication:根据ApplicationInfo创建Resources实例;我们需要做的就是在上面的ContainerActivity#onCreate中加载插件创建Apk时,使用这两个方法创建插件资源实例具体来说,首先使用PackageManager#getPackageArchiveInfo获取插件Apk的PackageInfo。有了PacakgeInfo之后,我们就可以自己组装一个ApplicationInfo,然后使用PackageManager#getResourcesForApplication创建一个资源实例。代码如下所示:PackageManagerpackageManager=getPackageManager();PackageInfopackageArchiveInfo=packageManager.getPackageArchiveInfo(pluginApkPath,PackageManager.GET_ACTIVITIES|PackageManager.GET_META_DATA|PackageManager.GET_SERVICES|PackageManager.GET_PROVIDERS|PackageManager.GET_SIGNATURES);packageArchiveInfo.applicationInfo.sourceDir=pluginApkPath;packageArchiveInfo.applicationInfo.publicSourceDir=pluginApkPath;ResourcesinjectResources=null;try{injectResources=packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);}catch(PackageManager.NameNotFoundExceptione){//...}拿到资源实例后,我们需要合并宿主资源和插件资源,写一个新的Resources类,这样完成自动代理:publicclassPluginResourcesextendsResources{privateResourceshostResources;privateResourcesinjectResources;publicPluginResources(ResourceshostResources,ResourcesinjectResources){super(injectResources.getAssets(),injectResources.getDisplayMetrics(),injectResources.getConfiguration());this.hostResources=hostResources;this.injectResources=injectResources;}@OverridepublicStringgetString(intid,Object...formatArgs)throwsNotFoundException{try{returninjectResources.getString(id,formatArgs);}catch(NotFoundExceptione){returnhostResources.getString(id,formatArgs);}}//...}然后我们在ContainerActivity加载完插件组件后创建一个MergeResources,然后重写ContainerActivity#getResources,替换获取的资源:publicclassContainerActivityextendsActivity{privateResourcespluginResources;@OverrideprotectedvoidonCreate(BundlessavedInstanceState){//...pluginResources=newPluginResources(super.getResources(),PluginLoader.getResources;pluginResources(plugin)/...}@OverridepublicResourcesgetResources(){if(pluginActivity==null){returnsuper.getResources();}returnpluginResources;}}这样就完成了注入tionofresources资源冲突,原因是不同插件中的资源id可能相同,所以解决方法是让不同的插件资源有不同的相同的资源id;resourceid用??8位十六进制数表示,表示为0xPPTTNNNNPP段,以区分包空间。默认情况下,它只区分应用程序资源和系统资源。TT段是资源类型,NNNN段在同一个APK中。从0000开始递增;综上所述,市面上其实有很多插件框架,比如腾讯的Shadow,滴滴的VirtualApk,360的RePlugin。他们各有长处,但大体相似;他们的一般原则实际上是相似的。运行的时候,进程中会运行一个宿主Apk,宿舍Apk才是真正安装的应用。宿主Apk可以加载插件Apk中的组件和组件。代码运行,插件Apk可任意热更新;