作者:ZYLAB文章概览发展历史常用名词使用Gradle简化插件开发流程类加载器:ClassLoader插件化实现难点四大组件的插件化实现一、发展历史2012年:AndroidDynamicLoader给予Fragment实现了插件化框架,可以动态加载插件中的Fragment实现页面的切换。2013年:阿里技术沙龙上,伯奎做了Atlas插件化框架的分享,说明那时候阿里已经在做插件化的运用和开发了。2014年:任玉刚开源了dynamicloadapk,通过代理分发的方式实现了动态化2015年:张勇发布了DroidPlugin,使用hook系统方式实现插件化。2017年:阿里推出Atlas2019年:腾讯推出了Shadow,号称是零反射,并且框架自身也可实现动态化,看了代码以后发现,其实本质上还是使用了代理分发生命周期实现四大组件动态化,然后抽象接口来实现框架的动态化。后面有机会可以对其做一下分析。 从2012至今,可以说插件化技术基本成型了,主要是代理和hook系统两种方式二、常用名词 在插件化中有一些专有名词,如果是第一次接触可能不太了解,这里解释一下。宿主:负责加载插件的apk,一般来说就是已经安装的应用本身。StubActivity:宿主中的占位Activity,注册在宿主Manifest文件中,负责加载插件Activity。PluginActivity:插件Activity,在插件apk中,没有注册在Manifest文件中,需要StubActivity来加载。三、使用gradle简化插件开发流程 在学习和开发插件化的时候,我们需要动态去加载插件apk,所以开发过程中一般需要有两个apk,一个是宿主apk,一个是插件apk,对应的就需要有宿主项目和插件项目。 在CommonTec这里创建了app作为宿主项目,plugin为插件项目。为了方便,我们直接把生成的插件apk放到宿主apk中的assets中,apk启动时直接放到内部存储空间中方便加载。这样的项目结构,我们调试问题时的流程就是下面这样:修改插件项目编译生成插件apk拷贝插件apk到宿主assets修改宿主项目编译生成宿主apk安装宿主apk验证问题如果每次我们修改一个很小的问题,都经历这么长的流程,那么耐心很快就耗尽了。最好是可以直接编译宿主apk的时候自动打包插件apk并拷贝到宿主assets目录下,这样我们不管修改什么,都直接编译宿主项目就好了。如何实现呢?还记得我们之前讲解过的gradle系列么?现在就是学以致用的时候了。首先在plugin项目的build。gradle添加下面的代码:project。afterEvaluate{project。tasks。each{if(it。nameassembleDebug){it。doLast{copy{fromnewFile(project。getBuildDir(),outputsapkdebugplugindebug。apk)。absolutePathintonewFile(project。getRootProject()。getProjectDir(),appsrcmainassets)renameplugindebug。apk,plugin。apk}}}}} 这段代码是在afterEvaluate的时候,遍历项目的task,找到打包task也就是assembleDebug,然后在打包之后,把生成的apk拷贝到宿主项目的assets目录下,并且重命名为plugin。apk。然后在app项目的build。gradle添加下面的代码:project。afterEvaluate{project。tasks。each{if(it。namemergeDebugAssets){it。dependsOn:plugin:assembleDebug}}} 找到宿主打包的mergeDebugAssets任务,依赖插件项目的打包,这样每次编译宿主项目的时候,会先编译插件项目,然后拷贝插件apk到宿主apk的assets目录下,以后每次修改,只要编译宿主项目就可以了。四、ClassLoader ClassLoader是插件化中必须要掌握的,因为插件是未安装的apk,系统不会处理其中的类,所以需要我们自己来处理。 4。1java中的ClassLoader BootstrapClassLoader负责加载JVM运行时的核心类,比如JAVAHOMElibrt。jar等等 ExtensionClassLoader负责加载JVM的扩展类,比如JAVAHOMElibext下面的jar包 AppClassLoader负责加载classpath里的jar包和目录 4。2android中的ClassLoader 在这里,我们统称dex文件,包含dex的apk文件以及jar文件为dex文件PathClassLoader用来加载系统类和应用程序类,可以加载已经安装的apk目录下的dex文件 DexClassLoader用来加载dex文件,可以从存储空间加载dex文件。 我们在插件化中一般使用的是DexClassLoader。 4。3双亲委派机制 每一个ClassLoader中都有一个parent对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果parent为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。下面是ClassLoader的loadClass方法的具体实现。protectedClasslt;?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{First,checkiftheclasshasalreadybeenloadedClasslt;?cfindLoadedClass(name);if(cnull){try{if(parent!null){先从父类加载器中进行加载cparent。loadClass(name,false);}else{cfindBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){ClassNotFoundExceptionthrownifclassnotfoundfromthenonnullparentclassloader}if(cnull){没有找到,再自己加载cfindClass(name);}}returnc;} 4。4如何加载插件中的类 要加载插件中的类,我们首先要创建一个DexClassLoader,先看下DexClassLoader的构造函数需要那些参数。publicclassDexClassLoaderextendsBaseDexClassLoader{publicDexClassLoader(StringdexPath,StringoptimizedDirectory,StringlibrarySearchPath,ClassLoaderparent){。。。}} 构造函数需要四个参数:dexPath是需要加载的dexapkjar文件路径optimizedDirectory是dex优化后存放的位置,在ART上,会执行oat对dex进行优化,生成机器码,这里就是存放优化后的odex文件的位置librarySearchPath是native依赖的位置parent就是父类加载器,默认会先从parent加载对应的类 创建出DexClassLaoder实例以后,只要调用其loadClass(className)方法就可以加载插件中的类了。具体的实现在下面:从assets中拿出插件apk放到内部存储空间privatefunextractPlugin(){varinputStreamassets。open(plugin。apk)File(filesDir。absolutePath,plugin。apk)。writeBytes(inputStream。readBytes())}privatefuninit(){extractPlugin()pluginPathFile(filesDir。absolutePath,plugin。apk)。absolutePathnativeLibDirFile(filesDir,pluginlib)。absolutePathdexOutPathFile(filesDir,dexout)。absolutePath生成DexClassLoader用来加载插件类pluginClassLoaderDexClassLoader(pluginPath,dexOutPath,nativeLibDir,this::class。java。classLoader)}五、插件化需要解决的难点 插件化,就是从插件中加载我们想要的类并运行,如果这个类是一个普通类,那么使用上面说到的DexClassLoader就可以直接加载了,如果这个类是特殊的类,比如说Activity等四大组件,那么就需要一些特殊的处理,因为四大组件是需要和系统进行交互的。插件化中,四大组件需要解决的难点如下:Activity生命周期如何调用如何使用插件中的资源Service生命周期如何调用BroadcastReceiver静态广播和动态广播的注册ContentProvider如何注册插件Provider到系统六、Activity的插件化实现 6。1难点分析 我们之前说到Activity插件化的难点,我们先来理顺一下为什么会有这两个问题。因为插件是动态加载的,所以插件的四大组件不可能注册到宿主的Manifest文件中,而没有在Manifest中注册的四大组件是不能和系统直接进行交互的。可能有些同学会问,那为什么不能直接把插件的Activity注册到宿主Manifest里呢?这样是可以,不过就失去了插件化的动态特性,如果每次插件中新增Activity都要修改宿主Manifest并且重新打包,那就和直接写在宿主中没什么区别了。我们再来说一下为什么没有注册的Activity不能和系统交互这里的不能直接交互的含义有两个系统会检测Activity是否注册如果我们启动一个没有在Manifest中注册的Activity,会发现报如下error:android。content。ActivityNotFoundException:Unabletofindexplicitactivityclass{com。zy。commonteccom。zy。plugin。PluginActivity};haveyoudeclaredthisactivityinyourAndroidManifest。xml? 这个log在Instrumentation的checkStartActivityResult方法中可以看到:publicclassInstrumentation{publicstaticvoidcheckStartActivityResult(intres,Objectintent){if(!ActivityManager。isStartResultFatalError(res)){return;}switch(res){caseActivityManager。STARTINTENTNOTRESOLVED:caseActivityManager。STARTCLASSNOTFOUND:if(intentinstanceofIntent((Intent)intent)。getComponent()!null)thrownewActivityNotFoundException(Unabletofindexplicitactivityclass((Intent)intent)。getComponent()。toShortString();haveyoudeclaredthisactivityinyourAndroidManifest。xml?);thrownewActivityNotFoundException(NoActivityfoundtohandleintent);。。。}}}Activity的生命周期无法被调用其实一个Activity主要的工作,都是在其生命周期方法中调用了,既然上一步系统检测了Manifest注册文件,启动Activity被拒绝,那么其生命周期方法也肯定不会被调用了。从而插件Activity也就不能正常运行了。 其实上面两个问题,最终都指向同一个难点,那就是插件中的Activity的生命周期如何被调用。解决问题之前我们先看一下正常系统是如何启动一个Activity的。这里对Activity的启动流程进行一些简单的介绍,具体的流程代码就不分析了,因为分析的话大概又能写一篇文章了,而且其实关于Activity的启动过程也有不少文章有分析了。这里放一张简图说明一下: 整个调用路径如下:Activity。startActivityInstrumentation。execStartActivityBinderAMS。startActivityActivityStarter。startActivityMayWaitstartActivityLockedstartActivityUnCheckedActivityStackSupervisor。resumeFocusedStackTopActivityLockedActivityStatk。resumeTopAcitivityUncheckLockedresumeTopActivityInnerLockedActivityStackSupervisor。startSpecificActivityLockedrealStartActivityLockedBinderApplictionThread。scheduleLauchActivityHActivityThread。scheduleLauchActivityhandleLaunchActivityperformLaunchActivityInstrumentation。newActivity创建ActivitycallActivityOnCreate一系列生命周期 其实我们可以把AMS理解为一个公司的背后大Boss,Activity相当于小职员,没有权限直接和大Boss说话,想做什么事情都必须经过秘书向上汇报,然后秘书再把大BossAMS的命令传达下来。而且大Boss那里有所有职员的名单,如果想要混入非法职员时不可能的。而我们想让没有在大Boss那里注册的编外人员执行任务,只有两种方法,一种是正式职员领取任务,再分发给编外人员,另一种就是欺骗Boss,让Boss以为这个职员是已经注册的。 对应到实际的解决方法就是:我们手动去调用插件Activity的生命周期欺骗系统,让系统以为Activity是注册在Manifest中的 说完生命周期的问题,再来看一下资源的问题在Activity中,基本上都会展示界面,而展示界面基本上都要用到资源。在Activity中,有一个mResources变量,是Resources类型。这个变量可以理解为代表了整个apk的资源。 在宿主中调用的Activity,mResources自然代表了宿主的资源,所以需要我们对插件的资源进行特殊的处理。我们先看一下如何生成代表插件资源的Resources类。首先要生成一个AssetManager实例,然后通过其addAssetPath方法添加插件的路径,这样AssetManager中就包含了插件的资源。然后通过Resources构造函数生成插件资源。具体代码如下:privatefunhandleResources(){try{首先通过反射生成AssetManager实例pluginAssetManagerAssetManager::class。java。newInstance()然后调用其addAssetPath把插件的路径添加进去。valaddAssetPathMethodpluginAssetManager?。javaClass?。getMethod(addAssetPath,String::class。java)addAssetPathMethod?。invoke(pluginAssetManager,pluginPath)}catch(e:Exception){}调用Resources构造函数生成实例pluginResourcesResources(pluginAssetManager,super。getResources()。displayMetrics,super。getResources()。configuration)} 前期准备的知识点差不多介绍完了,我们接着就看看具体的实现方法。 6。2手动调用Activity生命周期 手动调用生命周期原理如下图: 我们手动调用插件Activity生命周期时,需要在正确的时机去调用,如何在正确的时机调用呢?那就是启动一个真正的Activity,我们俗称占坑Activity(StubActivity),然后在StubActivity的生命周期里调用插件Activity对应的生命周期,这样就间接的启动了插件Activity。在StubActivity中调用插件Activity生命周期的方法有两种,一种是直接反射其生命周期方法,粗暴简单,唯一的缺点就是反射的效率问题。另外一种方式就是生成一个接口,接口里对应的是生命周期方法,让插件Activity实现这个接口,在StubActivity里就能直接调用接口方法了,从而避免了反射的效率低下问题。 具体的代码实现在CommonTec项目里可以找到,这里贴一下主要的实现(这里的实现和CommonTec里的可能会有些区别,CommonTec里有些代码做了一些封装,这里主要做原理的解释)。 6。2。1通过反射调用Activity生命周期 具体的实现见反射调用生命周期,下面列出了重点代码。classStubReflectActivity:Activity(){protectedvaractivityClassLoader:ClassLoader?nullprotectedvaractivityNameprivatevarpluginPathprivatevarnativeLibDir:String?nullprivatevardexOutPath:String?nulloverridefunonCreate(savedInstanceState:Bundle?){super。onCreate(savedInstanceState)nativeLibDirFile(filesDir,pluginlib)。absolutePathdexOutPathFile(filesDir,dexout)。absolutePathpluginPathintent。getStringExtra(pluginPath)activityNameintent。getStringExtra(activityName)创建插件ClassLoaderactivityClassLoaderDexClassLoader(pluginPath,dexOutPath,nativeLibDir,this::class。java。classLoader)}以onCreate方法为例,其他onStart等生命周期方法类似funonCreate(savedInstanceState:Bundle?){获取插件Activity的onCreate方法并调用getMethod(onCreate,Bundle::class。java)?。invoke(activity,savedInstanceState)}fungetMethod(methodName:String,varargparams:Class):Method?{returnactivityClassLoader?。loadClass(activity)?。getMethod(methodName,params)}} 6。2。2通过接口调用Activity生命周期 具体的实现见接口调用生命周期,下面列出了重点代码。通过接口调用Activity生命周期的前提是要定义一个接口IPluginActivityinterfaceIPluginActivity{funattach(proxyActivity:Activity)funonCreate(savedInstanceState:Bundle?)funonStart()funonResume()funonPause()funonStop()funonDestroy()} 然后在插件Activity中实现这个接口openclassBasePluginActivity:Activity(),IPluginActivity{varproxyActivity:Activity?nulloverridefunattach(proxyActivity:Activity){this。proxyActivityproxyActivity}overridefunonCreate(savedInstanceState:Bundle?){if(proxyActivitynull){super。onCreate(savedInstanceState)}}。。。} 在StubActivity通过接口调用插件Activity生命周期classStubInterfaceActivity:StubBaseActivity(){protectedvaractivityClassLoader:ClassLoader?nullprotectedvaractivityNameprivatevarpluginPathprivatevaractivity:IPluginActivity?nulloverridefunonCreate(savedInstanceState:Bundle?){super。onCreate(savedInstanceState)nativeLibDirFile(filesDir,pluginlib)。absolutePathdexOutPathFile(filesDir,dexout)。absolutePathpluginPathintent。getStringExtra(pluginPath)activityNameintent。getStringExtra(activityName)生成插件ClassLoaderactivityClassLoaderDexClassLoader(pluginPath,dexOutPath,nativeLibDir,this::class。java。classLoader)加载插件Activity类并转化成IPluginActivity接口activityactivityClassLoader?。loadClass(activityName)?。newInstance()asIPluginActivity?activity?。attach(this)通过接口直接调用对应的生命周期方法activity?。onCreate(savedInstanceState)}} 6。2。3资源处理方式 proxyresources 由于手动调用生命周期的方式,会重写大量的Activity生命周期方法,所以我们只要重写getResources方法,返回插件的资源实例就可以了。下面是具体代码。openclassStubBaseActivity:Activity(){protectedvaractivityClassLoader:ClassLoader?nullprotectedvaractivityNameprivatevarpluginPathprivatevarpluginAssetManager:AssetManager?nullprivatevarpluginResources:Resources?nullprivatevarpluginTheme:Resources。Theme?nullprivatevarnativeLibDir:String?nullprivatevardexOutPath:String?nulloverridefunonCreate(savedInstanceState:Bundle?){super。onCreate(savedInstanceState)nativeLibDirFile(filesDir,pluginlib)。absolutePathdexOutPathFile(filesDir,dexout)。absolutePathpluginPathintent。getStringExtra(pluginPath)activityNameintent。getStringExtra(activityName)activityClassLoaderDexClassLoader(pluginPath,dexOutPath,nativeLibDir,this::class。java。classLoader)handleResources()}overridefungetResources():Resources?{这里返回插件的资源,这样插件Activity中使用的就是插件资源了returnpluginResources?:super。getResources()}overridefungetAssets():AssetManager{returnpluginAssetManager?:super。getAssets()}overridefungetClassLoader():ClassLoader{returnactivityClassLoader?:super。getClassLoader()}privatefunhandleResources(){try{生成AssetManagerpluginAssetManagerAssetManager::class。java。newInstance()添加插件apk路径valaddAssetPathMethodpluginAssetManager?。javaClass?。getMethod(addAssetPath,String::class。java)addAssetPathMethod?。invoke(pluginAssetManager,pluginPath)}catch(e:Exception){}生成插件资源pluginResourcesResources(pluginAssetManager,super。getResources()。displayMetrics,super。getResources()。configuration)}} 6。3hook系统相关实现的方式欺骗系统,让系统调用生命周期 6。3。1hookInstrumentation 上面讲了如何通过手动调用插件Activity的生命周期方法来启动插件Activity,现在来看一下欺骗系统的方法。 上面简单介绍了Activity的启动流程,我们可以看到,其实Android系统的运行是很巧妙的,AMS是系统服务,应用通过Binder和AMS进行交互,其实和我们日常开发中客户端和服务端交互有些类似,只不过这里使用了Binder做为交互方式,关于Binder,可以简单看看这篇文章。我们暂时只要知道通过Binder应用可以和AMS进行对话就行。这种架构的设计方式,也为我们提供了一些机会。理论上来说,我们只要在启动Activity的消息到达AMS之前把Activity的信息就行修改,然后再消息回来以后再把信息恢复,就可以达到欺骗系统的目的了。 在这个流程里,有很多hook点可以进行,而且不同的插件化框架对于hook点的选择也不同,这里我们选择hookInstrumentation的方式进行介绍(原因是个人感觉这种方式要简单一点)。简化以后的流程如下: Instrumentation相当于Activity的管理者,Activity的创建,以及生命周期的调用都是AMS通知以后通过Instrumentation来调用的。我们上面说到,AMS相当于一个公司的背后大Boss,而Instrumentation相当于秘书,Activity相当于小职员,没有权限直接和大Boss说话,想做什么事情都必须经过秘书向上汇报,然后Instrumentation再把大BossAMS的命令传达下来。而且大Boss那里有所有职员的名单,如果想要混入非法职员时不可能的。不过在整个过程中,由于java的语言特性,大Boss在和秘书Instrumentation对话时,不会管秘书到底是谁,只会确认这个人是不是秘书(是否是Instrumentation类型)。 我们加载插件中的Activity,相当于让一个不在Boss名单上的编外职员去申请执行任务。在正常情况下,大Boss会检查职员的名单,确认职员的合法性,一定是通过不了的。但是上有政策,下有对策,我们悄悄的替换了秘书,在秘书和Boss汇报时,把职员名字改成大Boss名单中的职员,在Boss安排工作以后,秘书再把名字换回来,让编外职员去执行任务。而我们hook的方式就是替换调Instrumentation,修改Activity类名,达到隐瞒AMS的效果。 hook方式原理图: 接下来看看具体的代码实现。具体的实现见hook实现插件化,下面主要讲解重点代码。替换Instrumentation之前,首先我们要实现一个我们自己的Instrumentation,具体实现如下:classAppInstrumentation(varrealContext:Context,varbase:Instrumentation,varpluginContext:PluginContext):Instrumentation(){privatevalKEYCOMPONENTcommonteccomponentcompanionobject{funinject(activity:Activity,pluginContext:PluginContext){hook系统,替换Instrumentation为我们自己的AppInstrumentation,Reflect是从VirtualApp里拷贝的反射工具类,使用很流畅varreflectReflect。on(activity)varactivityThreadreflect。get(mMainThread)varbaseReflect。on(activityThread)。getInstrumentation(mInstrumentation)varappInstrumentationAppInstrumentation(activity,base,pluginContext)Reflect。on(activityThread)。set(mInstrumentation,appInstrumentation)Reflect。on(activity)。set(mInstrumentation,appInstrumentation)}}overridefunnewActivity(cl:ClassLoader,className:String,intent:Intent):Activity?{创建Activity的时候会调用这个方法,在这里需要返回插件Activity的实例valcomponentNameintent。getParcelableExtraComponentName(KEYCOMPONENT)varclazzpluginContext。classLoader。loadClass(componentName。className)intent。componentcomponentNamereturnclazz。newInstance()asActivity?}privatefuninjectIntent(intent:Intent?){varcomponent:ComponentName?nullvaroldComponentintent?。componentif(componentnullcomponent。packageNamerealContext。packageName){替换intent中的类名为占位Activity的类名,这样系统在Manifest中查找的时候就可以找到ActivitycomponentComponentName(com。zy。commontec,com。zy。commontec。activity。hook。HookStubActivity)intent?。componentcomponentintent?。putExtra(KEYCOMPONENT,oldComponent)}}funexecStartActivity(who:Context,contextThread:IBinder,token:IBinder,target:Activity,intent:Intent,requestCode:Int):Instrumentation。ActivityResult?{启动activity的时候会调用这个方法,在这个方法里替换Intent中的ClassName为已经注册的宿主ActivityinjectIntent(intent)returnReflect。on(base)。call(execStartActivity,who,contextThread,token,target,intent,requestCode)。get()}。。。} 在AppInstrumentation中有两个关键点,execStartActivity和newActivity。execStartActivity是在启动Activity的时候必经的一个过程,这时还没有到达AMS,所以,在这里把Activity替换成宿主中已经注册的StubActivity,这样AMS在检测Activity的时候就认为已经注册过了。newActivity是创建Activity实例,这里要返回真正需要运行的插件Activity,这样后面系统就会基于这个Activity实例来进行对应的生命周期的调用。 6。3。2hook系统的资源处理方式 因为我们hook了Instrumentation的实现,还是把Activity生命周期的调用交给了系统,所以我们的资源处理方式和手动调用生命周期不太一样,这里我们生成Resources以后,直接反射替换掉Activity中的mResource变量即可。下面是具体代码。classAppInstrumentation(varrealContext:Context,varbase:Instrumentation,varpluginContext:PluginContext):Instrumentation(){privatefuninjectActivity(activity:Activity?){valintentactivity?。intentvalbaseactivity?。baseContexttry{反射替换mResources资源Reflect。on(base)。set(mResources,pluginContext。resources)Reflect。on(activity)。set(mResources,pluginContext。resources)Reflect。on(activity)。set(mBase,pluginContext)Reflect。on(activity)。set(mApplication,pluginContext。applicationContext)fornativeactivityvalcomponentNameintent!!。getParcelableExtraComponentName(KEYCOMPONENT)valwrapperIntentIntent(intent)wrapperIntent。setClassName(componentName。packageName,componentName。className)activity。intentwrapperIntent}catch(e:Exception){}}overridefuncallActivityOnCreate(activity:Activity?,icicle:Bundle?){在这里进行资源的替换injectActivity(activity)super。callActivityOnCreate(activity,icicle)}}publicclassPluginContextextendsContextWrapper{privatevoidgenerateResources(){try{反射生成AssetManager实例assetManagerAssetManager。class。newInstance();调用addAssetPath添加插件路径MethodmethodassetManager。getClass()。getMethod(addAssetPath,String。class);method。invoke(assetManager,pluginPath);生成Resources实例resourcesnewResources(assetManager,context。getResources()。getDisplayMetrics(),context。getResources()。getConfiguration());}catch(Exceptione){e。printStackTrace();}}} 讲完上面两种方法,我们这里对比一下这两种方法的优缺点: 七、Service的插件化实现 Service比起Activity要简单不少,Service没有太复杂的生命周期需要处理,类似的onCreate或者onStartCommand可以直接通过代理分发。可以直接在宿主app里添加一个占位Service,然后在对应的生命周期里调用插件Service的生命周期方法即可。classStubService:Service(){varserviceName:String?nullvarpluginService:Service?nullcompanionobject{varpluginClassLoader:ClassLoader?nullfunstartService(context:Context,classLoader:ClassLoader,serviceName:String){pluginClassLoaderclassLoadervalintentIntent(context,StubService::class。java)intent。putExtra(serviceName,serviceName)context。startService(intent)}}overridefunonStartCommand(intent:Intent?,flags:Int,startId:Int):Int{valressuper。onStartCommand(intent,flags,startId)serviceNameintent?。getStringExtra(serviceName)pluginServicepluginClassLoader?。loadClass(serviceName)?。newInstance()asServicepluginService?。onCreate()returnpluginService?。onStartCommand(intent,flags,startId)?:res}overridefunonDestroy(){super。onDestroy()pluginService?。onDestroy()}overridefunonBind(intent:Intent?):IBinder?{returnnull}}八、BroadcastReceiver的插件化实现 动态广播的处理也比较简单,也没有复杂的生命周期,也不需要在Manifest中进行注册,使用的时候直接注册即可。所以只要通过ClassLoader加载插件apk中的广播类然后直接注册就好。classBroadcastUtils{companionobject{privatevalbroadcastMapHashMapString,BroadcastReceiver()funregisterBroadcastReceiver(context:Context,classLoader:ClassLoader,action:String,broadcastName:String){valreceiverclassLoader。loadClass(broadcastName)。newInstance()asBroadcastReceivervalintentFilterIntentFilter(action)context。registerReceiver(receiver,intentFilter)broadcastMap〔action〕receiver}fununregisterBroadcastReceiver(context:Context,action:String){valreceiverbroadcastMap。remove(action)context。unregisterReceiver(receiver)}}} 静态广播稍微麻烦一点,这里可以解析Manifest文件找到其中静态注册的Broadcast并进行动态注册,这里就不对Manifest进行解析了,知道其原理即可。九、ContentProvider的插件化实现 其实在日常开发中对于插件化中的ContentProvider使用还是比较少的,这里只介绍一种比较简单的ContentProvider插件化实现方法,就是类似Service,在宿主app中注册占位ContentProvider,然后转发相应的操作到插件ContentProvider中。代码如下:classStubContentProvider:ContentProvider(){privatevarpluginProvider:ContentProvider?nullprivatevaruriMatcher:UriMatcher?UriMatcher(UriMatcher。NOMATCH)overridefuninsert(uri:Uri?,values:ContentValues?):Uri?{loadPluginProvider()returnpluginProvider?。insert(uri,values)}overridefunquery(uri:Uri?,projection:ArrayoutString?,selection:String?,selectionArgs:ArrayoutString?,sortOrder:String?):Cursor?{loadPluginProvider()if(isPlugin1(uri)){returnpluginProvider?。query(uri,projection,selection,selectionArgs,sortOrder)}returnnull}overridefunonCreate():Boolean{uriMatcher?。addURI(com。zy。stubprovider,plugin1,0)uriMatcher?。addURI(com。zy。stubprovider,plugin2,0)returntrue}overridefunupdate(uri:Uri?,values:ContentValues?,selection:String?,selectionArgs:ArrayoutString?):Int{loadPluginProvider()returnpluginProvider?。update(uri,values,selection,selectionArgs)?:0}overridefundelete(uri:Uri?,selection:String?,selectionArgs:ArrayoutString?):Int{loadPluginProvider()returnpluginProvider?。delete(uri,selection,selectionArgs)?:0}overridefungetType(uri:Uri?):String{loadPluginProvider()returnpluginProvider?。getType(uri)?:}privatefunloadPluginProvider(){if(pluginProvidernull){pluginProviderPluginUtils。classLoader?。loadClass(com。zy。plugin。PluginContentProvider)?。newInstance()asContentProvider?}}privatefunisPlugin1(uri:Uri?):Boolean{if(uriMatcher?。match(uri)0){returntrue}returnfalse}} 这里面需要处理的就是,如何转发对应的Uri到正确的插件Provider中呢,解决方案是在Uri中定义不同的插件路径,比如plugin1的Uri对应就是content:com。zy。stubproviderplugin1,plugin2对应的uri就是content:com。zy。stubproviderplugin2,然后在StubContentProvider中根据对应的plugin分发不同的插件Provider。十、总结 本文介绍了插件化的相关实现,主要集中在Activity的实现上。重点如下: 最后推荐大家在学习插件化的同时,也去学习一些四大组件以及Binder的系统实现。 在这里就分享一份由大佬亲自收录整理的学习PDF架构视频面试文档源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这些都是我现在闲暇时还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效地帮助大家掌握知识、理解原理,帮助大家在未来取得一份不错的答卷。 当然,你也可以拿去查漏补缺,提升自身的竞争力。 真心希望可以帮助到大家,Android路漫漫,共勉! 如果你有需要的话,只需私信我【进阶】即可获取