一看就会 Android开发架构中[三化]的演变与示例

318 篇文章 19 订阅
46 篇文章 2 订阅
本文详细介绍了Android开发中的模块化、组件化和插件化概念。模块化强调高内聚、低耦合,方便代码复用;组件化进一步将业务模块独立,通过路由通信,提高开发效率和代码质量;插件化则允许未注册到主APK的代码动态加载,实现热更新。文章讨论了代理派和Hook派的插件化实现原理,并指出组件化与插件化在现代Android开发中的重要地位。
摘要由CSDN通过智能技术生成

作者:newki
转载地址:https://juejin.cn/post/7098915987311263781

对于整个Android开发来说,整体的开发架构离不开三化这个名词:模块化,组件化,插件化。

下面一起来看看它们是如何一步一步演变成今天的模样。

一. 模块化

软件开发不管是哪一个方向,大家都知道高内聚,低耦合的思想。

模块化也是每一门语言,各个方向的开发者都会处理的问题。在Android开发方向来说,模块化就是把常用的UI,网络请求,DB操作,第三方服务等公共的部分抽取封装成模块,方便复用。

再复杂一点,就是把大的业务拆分为小的业务模块,小的业务模块又依赖于公共的基础模块。打包成Apk的时候又会将不同的模块组合为一个整体,打包完成一个项目。看到这里,是不是就有一点组件化的雏形了。

模块化的好处和弊端

好处:

  • 复用,基础业务为业务模块复用,子业务为父业务复用。
  • 解耦,降低模块间的耦合,避免出现一个功能修改,需要到处找代码的尴尬局面。
  • 协作,团队的人员多了之后,不同的人员维护不同的模块,同步开发效率更高。

好处说破了天,它还是在单一的项目架构下,业务越来越复杂,业务模块越来越多,开发人员越来越多,模块化出现了一些问题。 弊端:

  • 编译慢,随着代码量越来越大,修改一个位置都需要几分钟甚至是十几分钟的编译,极大的降低开发效率。
  • 耦合,业务模块越来越多之后,小模块的功能修改,都需要对修改的代码耦合进行严谨的测试。
  • 冲突,开发人员不了解其他模块的功能,修改其中代码导致其他模块出问题,提交代码也容器出现冲突。

由此项目组件化走上舞台并成为当前开发项目的主流架构。

二. 组件化

组建化的核心思想就是把业务模块封装成一个一个的组件,可以供宿主依赖,也可以独立运行。而各个不同的组件由于是在不同的项目中,则通过路由来通信。 组件化的结构如下:

各模块的跳转和业务通信通过路由转发:

组件的配置一般分为两种方案: 方案一 在项目的build.gradle文件中配置是否作为application

    /**
     * module开关统一声明在此处
     * true:module作为application,可单独打包为apk
     * false:module作为library,可作为宿主application的组件
     */
    isNorthModule = false
    isSouthModule = false

然后在组件的build.gradle中配置plugin和AndroidManifest的路径:

    if (Boolean.valueOf(rootProject.ext.isModule_North)) {
        apply plugin: 'com.android.application'
    } else {
        apply plugin: 'com.android.library'
    }

    /*
    * java插件引入了一个概念叫做SourceSets,通过修改SourceSets中的属性,
    * 可以指定哪些源文件(或文件夹下的源文件)要被编译,
    * 哪些源文件要被排除。
    * */
    sourceSets {
        main {
            if (Boolean.valueOf(rootProject.ext.isModule_North)) {//apk
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java {
                    //library模式下,排除java/debug文件夹下的所有文件
                    exclude '*module'
                }
            }
        }
    }

方案二 组件就是Lib,想要独立运行模块,则建立对应的runalone模块为application再依赖指定的模块。无需配置开关,无需编译。 大致项目结构如下:

运行的时候可以选择运行哪一个模块:

组件化的出现,解决了模块化的痛点。带来如下优势:

  1. 加快业务迭代速度,各个业务模块组件更加独立,不再出现业务耦合情况。
  2. 稳定的公共模块采用依赖库方式,提供给各个业务线使用,减少重复开发和维护工作量。
  3. 迭代频繁的业务模块采用组件方式,各业务研发可以互不干扰、提升协作效率,并控制产品质量。
  4. 为新业务随时集成提供了基础,所有业务可上可下,灵活多变。
  5. 降低团队成员熟悉项目的成本,降低项目的维护难度。
  6. 加快编译速度,提高开发效率。
  7. 控制代码权限,将代码的权限细分到更小的粒度。

PS.这里只是简单了解一下组件化的架构,关于组件化,后面我会出一期单独讲一下。

组件化已经如此方便,但是在快速迭代的今天,如果出现Bug修复,出了新功能急需上线,还要等待应用商店审核才能上线,实在是让各大厂难受,也是由于对业务组件/模块的热更新提出了更高的要求,由此插件化登上了舞台。

二. 插件化

插件化的本质就是加载一些没有注册到本地apk的一些插件代码。

插件化是2012年提出到2014年成熟,至今插件化已经迭代几代了。技术更成熟了。主要分为2个流派 代理派和Hook派

以RePlugen和Shadow为首的代理派。 以DroidPlugin为首的Hook派。

代理派的原理: DexClassLoader加载插件dex代码。反射生成AssetManager实例,调用 Resources构造函数生成实例。使用接口的方式手动回调插件的生命周期。 伪代码如下: 生成DexClassLoader:

    // 从 assets 中拿出插件 apk 放到内部存储空间
    private fun extractPlugin() {
        var inputStream = assets.open("plugin.apk")
        File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
    }

    private fun init() {
        extractPlugin()
        pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
        nativeLibDir = File(filesDir, "pluginlib").absolutePath
        dexOutPath = File(filesDir, "dexout").absolutePath
        // 生成 DexClassLoader 用来加载插件类
        pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
    }

处理Resource:

private fun handleResources() {
    try {
        // 首先通过反射生成 AssetManager 实例
        pluginAssetManager = AssetManager::class.java.newInstance()
        // 然后调用其 addAssetPath 把插件的路径添加进去。
        val addAssetPathMethod = pluginAssetManager?.javaClass?.getMethod("addAssetPath", String::class.java)
        addAssetPathMethod?.invoke(pluginAssetManager, pluginPath)
    } catch (e: Exception) {
    }
    // 调用 Resources 构造函数生成实例
    pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration)
}

通过接口调用插件Activity生命周期:

interface IPluginActivity {
    fun attach(proxyActivity: Activity)
    fun onCreate(savedInstanceState: Bundle?)
    fun onStart()
    fun onResume()
    fun onPause()
    fun onStop()
    fun onDestroy()
}
复制代码

插件Activity实现接口

open class BasePluginActivity : Activity(), IPluginActivity {
    var proxyActivity: Activity? = null

    override fun attach(proxyActivity: Activity) {
        this.proxyActivity = proxyActivity
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        if (proxyActivity == null) {
            super.onCreate(savedInstanceState)
        }
    }
    // ...
}

在 StubActivity 通过接口调用插件 Activity 生命周期

open class StubBaseActivity : Activity() {

    protected var activityClassLoader: ClassLoader? = null
    protected var activityName = ""
    private var pluginPath = ""
    private var pluginAssetManager: AssetManager? = null
    private var pluginResources: Resources? = null
    private var pluginTheme: Resources.Theme? = null
    private var nativeLibDir: String? = null
    private var dexOutPath: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        nativeLibDir = File(filesDir, "pluginlib").absolutePath
        dexOutPath = File(filesDir, "dexout").absolutePath
        pluginPath = intent.getStringExtra("pluginPath")
        activityName = intent.getStringExtra("activityName")

         // 生成插件 ClassLoader
        activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
        // 加载插件 Activity 类并转化成 IPluginActivity 接口
        activity = activityClassLoader?.loadClass(activityName)?.newInstance() as IPluginActivity?
        activity?.attach(this)
        // 通过接口直接调用对应的生命周期方法
        activity?.onCreate(savedInstanceState)

        handleResources()
    }

    override fun getResources(): Resources? {
        // 这里返回插件的资源,这样插件 Activity 中使用的就是插件资源了
        return pluginResources ?: super.getResources()
    }

    override fun getAssets(): AssetManager {
        return pluginAssetManager ?: super.getAssets()
    }

    override fun getClassLoader(): ClassLoader {
        return activityClassLoader ?: super.getClassLoader()
    }

    private fun handleResources() {
        try {
            // 生成 AssetManager
            pluginAssetManager = AssetManager::class.java.newInstance()
            // 添加插件 apk 路径
            val addAssetPathMethod = pluginAssetManager?.javaClass?.getMethod("addAssetPath", String::class.java)
            addAssetPathMethod?.invoke(pluginAssetManager, pluginPath)
        } catch (e: Exception) {
        }
        // 生成插件资源
        pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration)
    }
}

而腾讯Shadow和上面的有点区别,它做了一个非常简单事,通过运用AOP思想,利用字节码编辑工具,在编译期把插件中的所有Activity的父类都改成一个普通类,然后让壳子持有这个普通类型的父类去转调它就不用Hack任何系统实现了。虽然说是非常简单的事,实际上这样修改后还带来一些额外的问题需要解决,比如getActivity()方法返回的也不是Activity了。不过Shadow的实现中都解决了这些问题。

Hook派的原理: 先了解Activity的启动流程

Activity.startActivity -> Instrumentation.execStartActivity -> Binder -> AMS.startActivity -> ActivityStarter.startActivityMayWait 
-> startActivityLocked -> startActivityUnChecked -> ActivityStackSupervisor.resumeFocusedStackTopActivityLocked 
-> ActivityStatk.resumeTopAcitivityUncheckLocked -> resumeTopActivityInnerLocked -> ActivityStackSupervisor.startSpecificActivityLocked 
-> realStartActivityLocked -> Binder -> ApplictionThread.scheduleLauchActivity -> H -> ActivityThread.scheduleLauchActivity 
-> handleLaunchActivity -> performLaunchActivity -> Instrumentation.newActivity 创建 Activity -> callActivityOnCreate 一系列生命周期

我们可以把 AMS 理解为一个公司的背后「大 Boss」,Activity 相当于「小职员」,没有权限直接和大 Boss 说话,想做什么事情都必须经过「秘书」向上汇报,然后秘书再把大 Boss AMS 的命令传达下来。而且大 Boss 那里有所有职员的名单,如果想要混入非法职员时不可能的。而我们想让没有在大 Boss 那里注册的编外人员执行任务,只有两种方法,一种是正式职员领取任务,再分发给编外人员,另一种就是欺骗 Boss,让 Boss 以为这个职员是已经注册的。

对应到实际的解决方法就是:

1.我们手动去调用插件 Activity 的生命周期。
2.欺骗系统,让系统以为 Activity 是注册在 Manifest 中的。

大致步骤:实现一个我们自己的 Instrumentation

class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) :
    Instrumentation() {
    private val KEY_COMPONENT = "commontec_component"

    companion object {
        fun inject(activity: Activity, pluginContext: PluginContext) {
            // hook 系统,替换 Instrumentation 为我们自己的 AppInstrumentation,Reflect 是从 VirtualApp 里拷贝的反射工具类,使用很流畅~
            var reflect = Reflect.on(activity)
            var activityThread = reflect.get<Any>("mMainThread")
            var base = Reflect.on(activityThread).get<Instrumentation>("mInstrumentation")
            var appInstrumentation = AppInstrumentation(activity, base, pluginContext)
            Reflect.on(activityThread).set("mInstrumentation", appInstrumentation)
            Reflect.on(activity).set("mInstrumentation", appInstrumentation)
        }
    }

    override fun newActivity(cl: ClassLoader, className: String, intent: Intent): Activity? {
        // 创建 Activity 的时候会调用这个方法,在这里需要返回插件 Activity 的实例
        val componentName = intent.getParcelableExtra<ComponentName>(KEY_COMPONENT)
        var clazz = pluginContext.classLoader.loadClass(componentName.className)
        intent.component = componentName
        return clazz.newInstance() as Activity?
    }

    private fun injectIntent(intent: Intent?) {
        var component: ComponentName? = null
        var oldComponent = intent?.component
        if (component == null || component.packageName == realContext.packageName) {
            // 替换 intent 中的类名为占位 Activity 的类名,这样系统在 Manifest 中查找的时候就可以找到 Activity
            component = ComponentName("com.zy.commontec", "com.zy.commontec.activity.hook.HookStubActivity")
            intent?.component = component
            intent?.putExtra(KEY_COMPONENT, oldComponent)
        }
    }

    fun execStartActivity(
        who: Context,
        contextThread: IBinder,
        token: IBinder,
        target: Activity,
        intent: Intent,
        requestCode: Int
    ): Instrumentation.ActivityResult? {
        // 启动 activity 的时候会调用这个方法,在这个方法里替换 Intent 中的 ClassName 为已经注册的宿主 Activity
        injectIntent(intent)
        return Reflect.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 实例来进行对应的生命周期的调用。

因为我们 hook 了 Instrumentation 的实现,还是把 Activity 生命周期的调用交给了系统,所以我们的资源处理方式和手动调用生命周期不太一样,这里我们生成 Resources 以后,直接反射替换掉 Activity 中的 mResource 变量即可。下面是具体代码:

class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) : Instrumentation() {
    private fun injectActivity(activity: Activity?) {
        val intent = activity?.intent
        val base = activity?.baseContext
        try {
            // 反射替换 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)
            // for native activity
            val componentName = intent!!.getParcelableExtra<ComponentName>(KEY_COMPONENT)
            val wrapperIntent = Intent(intent)
            wrapperIntent.setClassName(componentName.packageName, componentName.className)
            activity.intent = wrapperIntent
        } catch (e: Exception) {
        }
    }

    override fun callActivityOnCreate(activity: Activity?, icicle: Bundle?) {
        // 在这里进行资源的替换
        injectActivity(activity)
        super.callActivityOnCreate(activity, icicle)
    }
}

public class PluginContext extends ContextWrapper {
    private void generateResources() {
        try {
            // 反射生成 AssetManager 实例
            assetManager = AssetManager.class.newInstance();
            // 调用 addAssetPath 添加插件路径
            Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
            method.invoke(assetManager, pluginPath);
            // 生成 Resources 实例
            resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

虽然插件化的原理大家都了解了,但是实战中还是推荐大家使用开源框架来实现如以RePlugen Shadow DroidPlugin 等。各个框架各有利弊,还望大家体验之后再做选择。

四. 总结

总的来说,目前的开发是以组件化开发为主,组件内部的功能也离不开模块化的思想,在组件化的结构上,很方便的实现插件化,以各个业务组件为一个一个的单独插件,方便修复问题上线新功能。组合起来,这也是当前主流的Android开发架构了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值