从大厂APP看Android组件化实践最终篇

Posted 初一十五啊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从大厂APP看Android组件化实践最终篇相关的知识,希望对你有一定的参考价值。

前言

关于组件化的介绍,今天就完结了。

后续等组件化更新完,更新

  • Shadow插件化
  • ASM其他
  • Systrace使用与解析
  • 字节码相关

关注公众号:初一十五a
解锁 《Android十三大板块文档》,让学习更贴近未来实战。已形成PDF版

内容如下

1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试应有尽有
3.Android车载应用大合集,从零开始一起学
4.性能优化大合集,告别优化烦恼
5.Framework大合集,从里到外分析的明明白白
6.Flutter大合集,进阶Flutter高级工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对工作需求
10.Android基础篇大合集,根基稳固高楼平地起
11.Flutter番外篇:Flutter面试+Flutter项目实战+Flutter电子书
12.高级Android组件化强化实战
13.十二模块之补充部分:其他Android十一大知识体系

整理不易,关注一下吧。开始进入正题,ღ( ´・ᴗ・` ) 🤔

从有赞微商城看组件化架构实践

组件化改造概述

  • 提出了 5 个调整方向:抽象基础模块、公共服务去中心化、业务模块服务化、抽象基础组件、单/多模块打包
  • 介绍了基于 3 个基础组件依赖和 1 个 Gradle 插件的落地方案

一丶有赞移动应用如何给页面安上“任意门”

“任意门”:一行配置实现页面跳转重定向。

1.1.背景 & 痛点 & 价值

动态路由组件,处理的是 App 中最最常见的一种行为的问题,那就是:跳转。

随着 App 技术栈的扩展,从原本最最简单的原生到原生的跳转,扩展到目前同一个 App 中包含原生页面、H5 页面、Weex 页面、Flutter 页面之间的跳转。

随之而来的问题就是:随着 App 的版本迭代,很多原本原生实现的页面,需要通过新的 H5 或者 Weex 页面进行升级/降级。而这些原本都是硬编码的跳转逻辑,可能需要随着版本不停改动。总结下来,现有的,各个技术栈隔离的页面跳转逻辑面临的直接问题有:

  • 跳转逻辑跟着版本走,无法统一进行改动
  • 跨技术栈跳转的实现成本比较高,必须在桥接模块中进行特殊适配
  • 在一些 H5 需要使用专门 WebView 页面打开的场景下,很难去适配,也必须通过各个 Web 跳转的拦截做特殊处理

为了解决以上硬编码以及灵活性差的问题,我们决定梳理现有的各技术栈跳转逻辑,将这些跳转整合,能够满足动态性、可配置的需求。

得益于项目中原有的路由跳转组件,各种页面之间的页面都可以通过 URL 的方式进行路由,于是我们基于 URL 跳转,开发了一套动态路由组件,它完成的工作有 :

  • 承担 App 内所有跳转逻辑
  • 通过配置中心组件,支持获取/配置路由替换规则
  • 匹配所有的路由跳转规则,命中规则的,替换成新的目标路由地址
  • 将实际跳转目标地址传递给路由组件执行实际的跳转行为

1.2.实现方案

路由拦截+替换
微商城客户端目前已经有一套稳固的组件化实现方案,组件之间的页面跳转通过路由的方式进行解耦,这是一种比较常见的方式。

在微商城项目中,负责实现的路由组件为 ZanURLRouter ,它的职责很简单:

  • 启动时注册路由和页面
  • 找寻正确的页面进行跳转

    在不影响外部接口的前提下,我们在目标路由解析这一步,引入了动态路由

    对于移动端的路由重定向,实际上就是将一个路由转换为另一个路由,如:
youzan://orderlist?type=1&status=2

转换为:

wsc://orderlist/v2?type=1&status=2

跳转规则配置
路由的拦截和替换中的一个关键节点就是“配置”,我们需要一个路由规则列表来记录和下发匹配规则。为了方便下发路由规则表,我们将这份配置表存放在有赞移动配置中心,根据客户端的版本进行区分,动态地下发给不同版本的客户端。

一条路由规则,分为一个 Key 和对应的 Value,Key 为匹配方式,使用正则表达式进行匹配,Value 为替换方式,使用 JSON 格式定义。

实际代码实现中,我们将“路由规则”和“路由替换行为”分别抽象成实体类和接口方法。

抽象实体类

关于替换路由跳转的规则,我们可以这样配置:

Key: ^youzan://orderlist\\?type=(\\d+)&status=(\\d+)$
Value: 
        "template": "wsc://orderlist/v2?type=$1&status=$2"

即:一条匹配规则 + 一条替换模板。我们将之抽象为一个实体类, Rule :

class Rule 
    // url 匹配规则(正则表达式)
    String pattern;
    // url 匹配规则(正则表达式)
    String template;

抽象接口

有了规则配置之后,就需要对动态路由的行为进行抽象,核心就是初始化规则、匹配规则和替换路由三个方法:

// 注册替换规则
fun initWithPattern(Rule rule)
// 校验是否命中已经注册的路由配置的 pattern 正则
fun testWithRoute(String routeUrl): Boolean
// 获取替换后的跳转地址
fun appliedWithRoute(String routeUrl): String

动态路由器会在应用启动阶段拉取正确的规则表,解析并记录下来:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dHTAWo77-1667814487712)(https://upload-images.jianshu.io/upload_images/23851953-4fc9f25bfd9546b3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
ZanURLRouter 解析目标路由的时候,对每一个规则进行匹配测试,命中则应用匹配的规则,返回替换后的路
由,再继续接下来的工作。

路由替换

实体类、接口类都抽象完成之后,就是动态路由的核心实现了,这里依赖到一个的核心工具就是:正则表达式。这里用到正则的场景有两个: - 正则验证是否命中规则 - 正则替换url文本

androidios 开发中,字符串正则相关的 API 都是自带的,开箱即用:

/* ------------ Android ------------ */

// 正则匹配校验方法
Pattern.matcher(String text)
// 正则匹配校验方法
Regex.replace(String input, String replacement)

/* ------------ iOS ------------ */
(NSString *)stringByReplacingMatchesInString:(NSString *)string options:
(NSMatchingOptions)options range:(NSRange)range withTemplate:(NSString *)templ;

疑难问题:参数处理

大部分情况下,跳转本身都是带参数的,那么动态替换跳转的 URL 之后,参数的获取就成了一个问题,尤其是原生和其他页面页面的跳转。

我们主要以 Android 为例,Android 原生跳转都是通过一个关键类:Intent 来实现参数的存取。这里需要注意的是,由于 Intent 传值存在多种复杂的数据接口,包括 Parcelable 这种复杂参数的场景,由于降级之后都是以 URL 的形式传值,所以我们目前约定动态路由的参数只支持基本数据类型,复杂参数类型的需要接入方来做兼容。

参数处理我们分两个典型的场景来讨论: - 原生跳转 H5 参数传递 - H5 跳转原生的参数传递.

原生跳转H5

这里的方式主要是将 Intent 中的基本数值类型参数取出来,拼接成带参数的 URL 来实现将 Intent 里面的参数传递给H5,主要实现代码如下:

fun appendBundleParams(strBuilder: StringBuilder, bundle: Bundle) 
    val ketSet = bundle.keySet()
    for (key in ketSet) 
        bundle[key]?.let  value ->
           when (value) 
               is Bundle -> appendBundleParams(strBuilder, value)
               is String, is Int, is Long, is Double, is Float, is Char, is Boolean, is Short -> 
                   if (strBuilder.isNotEmpty()) 
                       strBuilder.append("&")
                   
                   strBuilder.append("$key=$value")
               
               else -> 
                   // do nothing
               
            
        
    

H5跳转原生

同理的,H5 跳转原生做的就是将 URL 中携带的参数塞到 Intent 中来进行。

这里比较关键的一个问题是:Intent 的取值都是带类型的,而 URL 的参数都是字符串。我们目前解决方案也很简单,就是封装 Intent 的取值方法,由于目前有赞 Android 主要使用 Kotlin 来开发,可以使用 Kotlin 的扩展函数特性来实现(Java 可以使用工具类的方式):

fun Intent.getIntFromRouter(key: String, defaultValue: Int): Int 
    val extras = this.extras;
    if (extras == null || !this.hasExtra(key)) 
        return defaultValue
    
    return extras.get(key) as? Int ?: (this.getStringExtra(key)?.toInt() ?: defaultValue)

碰到的坑:UrlEncode

在匹配和替换 URL 规则的场景中,我们经常会碰到这么一种情况,URL 是被 UrlEncode 过的。由于字符串的正则匹配和正则替换是不会判断字符串是否被 UrlEncode 过,所以这里的逻辑需要由路由组件来实现。

UrlEncode 字符串的正则匹配逻辑实现比较简单,即直接将字符串 Decode 之后进行匹配。

比较复杂的是 UrlEncode 字符串的正则替换,有些情况下,路由中的url是必须进行 UrlEncode 的,如果直接Decode 进行替换,那么可能会导致实际跳转的目标 URL 被错误地截断,导致无法跳转,所以这里的替换必须保留UrlEncode 的字符。

我们的解决思路是:记录 URLEncode 前后被 encode 字符的下标,然后再手动实现 replace 方法去挨个替换字符串中的字符,核心代码如下:

private fun getEncodeCharMap(url: String, encodeUrl: String): Map<Int, IntRange> 
    if (Uri.decode(encodeUrl) != url) 
        return mapOf()
    
    val urlChars = url.toCharArray()
    val urlEncodeChars = encodeUrl.toCharArray()
    var i = 0
    var j = 0
    val encodeMap = mutableMapOf<Int, IntRange>()
    while (i < urlChars.size && j < urlEncodeChars.size) 
       // text: [www:] => [www%23]
       // length: [0123] => [012345]
       if (urlChars[i] != urlEncodeChars[j]) 
           val s = Uri.encode(urlChars[i].toString())
           val range = IntRange(j, j + s.length - 1)
           encodeMap[i] = range
           j += s.length
        else 
           j++
       
       i++
    
    return encodeMap

1.3.实际应用案例

应用中心

微商城App应用中心,应该是应用动态路由的最佳场景,应用中心存在大量跳转的场景。

先来说下使用动态路由的背景,应用中心中应用列表都是由服务端统一下发的,后端为每个应用配置的跳转地址是统一的,而 Android 和 iOS 本地路由配置的 URL 是不一致的,如果直接下发配置的话,会存在有一端无法跳转的问题。以店铺管理应用跳转为例:

  • iOS中店铺管理的路由 URL:wsc://shop/management
  • Android 中的路由URL:wsc://team/management
  • 服务端下发的URL:wsc://team/management

那么解决同一套配置跳转不同 URL 的这个问题,就交给动态路由来完成了,我只需要在iOS的动态路由添加一个规则,将 wsc://shop/management 动态替换成 wsc://team/management 就可以搞定!

订单项目

在微商城客户端的订单模块重构项目中,考虑到订单是使用频次很高的核心场景之一,且代码历史较久,所以新的模块上线后与旧订单列表模块共存,直到灰度完全结束。

由于微商城已经是组件化拆分,业务组件之间的跳转使用路由完成,我们在设计灰度方案时,利用动态路由来实时进行目标路由的映射:

1.4.总结

“上线只是开始”,随着业务迭代,历史业务也越来越多,为了保证不同平台版本的用户能够平滑过渡到新的功能上去,动态路由组件扮演了一个客户端的 URL 重定向服务的角色,避免因服务下线、功能更新、平台差异、项目重构等原因导致的功能不可用。

动态路由组件,核心就是非常简单的正则匹配和正则替换,而这个非常简单和核心代码逻辑,实现了业务场景下非常重要的路由重定向。这整套解决方案,也是有赞移动端在应用组件化、动态化的一个重要组成部分,我们也希望这个技术方案能够抛砖引玉,启发更多优秀的移动端动态化解决思路。

二丶有赞微商城-Android组件化方案

2.1.概述

目前有赞移动端的主要工作内容是在“有赞微商城”和“有赞零售”两条公司主要的业务线,随着有赞 Saas 业务的增长,客户端也不断迭代,支持越来越多的功能。 在这个业务快速增长的情况下,移动端技术的整体架构也是一直在不断调整,来保证开发效率和业务的快速迭代。

这篇文章,主要是介绍有赞微商城 Android组件化的一些思路和实现。

现状
客户端的架构,从一开始的“All IN ONE” 模式(即所有代码都在 App 中),逐渐演变到目前的一个单 Project 多Module 结构:

痛点

新的项目架构,也带了了新的问题: - 日益复杂的 Common 模块,逻辑复杂,依赖不清晰,不敢随便改动Common 代码,造成大量冗余代码和无法维护的业务逻辑 - 随着业务模块的增多,打包速度一发不可收拾;从倒杯水的时间到下楼吃个饭的时间,大大减慢了开发节奏 - 由于业务模块跟项目中的上层(App 壳)和下层(Common模块)耦合 - 业务模块增多,由于业务模块没有自己的生命周期,无法实现模块之间的隔离,整体模块控制比较混乱

需要解决的问题

  • Common模块轻量化,需要将 Common 层的业务向上抽离,通用的底层和基础组件、公用 UI 组件抽成单独的依赖
  • 移动端业务服务化,解耦现有业务,抽象出业务接口,业务模块只向外暴露自己的接口,并实现跨模块之间的调用
  • 能够配置单模块或者多模块打包,不用每次调试都全量打包,费时费力,又影响开发的节奏
  • 业务模块的依赖和发布管理

2.2.架构调整

我们之前虽然有在做整个工程模块化的开发,但是目前的模块化框架可以说是不够彻底的:

  • 模块只是项目结构的概念(一个模块一个 Module),在逻辑层并没有模块这个概念
  • 模块本身并没有生命周期控制
  • 公用服务中心化,公用逻辑部分全部都在 Common 模块中
  • 模块对外暴露的服务不可知,都是直接依赖模块内部的代码逻辑
  • 模块无法单独打包,针对模块的代码改动,只能全量打包之后才能看到效果
  • 为了解决以上的问题,我们需要对现有的架构进行调整。

模块化的抽象

将模块的功能抽象出一些基础类,组成了模块化支持组件,它提供的功能有:

  • 抽象出模块本身作为某一类业务的容器,即所有业务模块需要实现自己的模块类,继承自我们的BaseModule,并在 App 壳工程中进行注册
  • 模块对象跟 Activity 一样,拥有生命周期的概念,需要在生命周期的不同阶段处理自己相应的逻辑(注册服务、初始化数据等)
  • 模块可以注册的对外暴露的服务的实现,在注册模块的时候,模块携带的服务也会被注册到 App 的服务中心

公共业务去中心化

跟很多客户端的同学聊过,很多 APP 发展到一定阶段之后,必然会诞生一个所谓的 Common 模块。它就像一个大储物柜,每个人都把一些其他人可能用到的东西一股脑儿塞进去。 这么个塞法,会有两个问题:

  1. 冗余:比如一些工具类,很多时候,当你找不到需要的工具类的时候,你可能会塞一个新的进去
  2. 维护成本高:所有公用的业务逻辑的实现都在 Common 中,对一些公用业务逻辑的影响面无法掌控

Common 里面都有什么?

  • 工具类
  • 公用的 UI 组件
  • 多个业务模块都公用的业务类
  • 基础组件的封装类(图片库、网络库、Webview)
  • 封装的一些基类(BaseActivity,BaseFragment 什么的)

解决的思路

  • 将公用的业务模块向上抽离到业务模块中(所谓业务模块的服务化)
  • 将基础组件抽象到一个独立的组件中
  • 将一些基础类下沉到不包含业务逻辑的底层核心库中

业务模块服务化
服务化”这个词,在服务端的开发中经常被提到,简单来说,就是根据业务划分为多个模块,模块之间的交互以互相提供服务的方式来完成。 而客户端随着业务模块的增多,也必然存在业务模块之间存在业务依赖的情况,而Android 端依赖的方式有:

  1. A 模块直接依赖 B 模块,直接调用 B 模块的代码逻辑
  2. 将 A 和 B 模块中的公用部分放到 Common 模块中,通过调用 Common 模块的代码实现依赖

业务模块服务依赖的实现

  • 后端的服务化是借助于 Dubbo 来构建的 RPC 服务,依赖某个服务,只需要依赖其对外暴露的 API 模块(只包含接口和数据结构的 Maven 依赖),不需要依赖其具体实现,具体服务调用的实现由框架来实现
  • 客服端的依赖也可以参考这样的方式来实现模块之间的依赖,例如商品模块,可以提供一个 API 层,用来对外暴露数据结构和服务

API 层实现方式

对外暴露服务的方式有很多种:

  • 协议的方式:例如"app://order/detail/get?id=100",数据可以用 JSON 来进行传递,请求本地服务就像调用一个 Http 服务一样,根据请求协议来获取数据,然后解析数据进行操作
  • 接口的方式:像后端使用 Dubbo 服务那样,订单模块对外提供一个独立的 Maven 依赖,里面包含了数据接口和对外提供的服务接口,适用方依赖之后直接调用

接口的方式实现 API

协议的方式的问题:如果服务提供的地方更改了之后,需要手动去查询所有调用到的地方,进行更改,而且没有版本管理,而且数据解析都需要手动进行转换,改动的成本比较高,也有一定稳定性风险。 接口的方式的问题:需要额外提供一个依赖(单独把 API 层打包成一个 aar 包),使用方需要添加 Mave 依赖,所以引入依赖和发布的成本比较高。

我们最终选择了接口的方式,这种方式的稳定性和版本控制做的更好,对于改动来说,编译过程自动会帮你校验改动的影响面,而引入依赖和发布成本高的问题,完全可以交给构建工具(Gradle Plugin)来解决。

业务实现层

业务实现层需要做的,就是实现自己模块本身的业务逻辑,并实现自己提供的 API 接口,暴露对外的服务。

基础组件抽象

现有的基础组件实现

项目中现在有很多的基础组件都是统一在 Common 里面进行封装的,例如:账号库、网络库、图片加载、Web 容器库等等,这也带来了一些问题:

  1. Common 太重
  2. 业务模块跟基础组件强耦合,在开发一些跨团队的组件过程中,如果碰到使用的基础库不同的时候,需要比较多的时间来做封装
  3. 升级基础组件或替换依赖的成本比较高,一些 API 的更改需要改动每个调用的地方

实现思路

  • 将常用的基础组件整理,抽象成单独的一个抽象层,里面定义了一系列基础组件接口(图片加载、Web 容器、JsBridge 调用、账号等等)
  • 把统一实现的组件放到另一个依赖里面,可以在 App 中进行具体实现的注册,而业务模块本身,可以只依赖抽象

依赖结构

单/多模块打包
随着业务量和业务复杂度的增长,还有多个三方组件的引入,客户端工程代码量也变得越来越庞大,直接造成的一个问题是:打包慢!一个简单的场景:当你开发了一个商品模块内部的功能之后,你需要打整个 App 的包才能进行测试,而打一个包的时间可能是 5~10 分钟,如果一天打包 10 次,也是比较酸爽。我们的组件也需要支持单模块或者选定的某些进行打包,其中的思路也是通过自定义 Gradle Plugin 在编译阶段,动态去更改 Module 实际依赖的Android Gradle 插件来实现的。 经测试,同一台电脑,完整打包(clean之后再安装)耗时 4 分钟,而单模块打包
(同样也是 clean 之后安装)耗时 1 分钟,整体打包时间降低了 70% 以上。

架构图

上面的一些改进点,总结成一张图,就是这样的:

2.3.实现方案

目前我们的方案提供 3 个基础组件依赖和 1 个 Gradle 插件:

  • modular-core: 提供组件模块化生命周期和模块服务注册相关的模块化基础组件
  • modular-support: 对项目中二方、三方包接口的抽象
  • modular-support-impl:对项目中二方、三方包接口的默认抽象
  • modular-plugin: 支持模块生成 API 层目录,生成 APP 运行环境,以及管理模块发布的 Gradle 插件

Modular-core

实现模块类

业务模块类需要继承 BaseModule:

public class ModuleA extends BaseModule 
       
    @Override
    public void onInstalled() 
        registerBusinessService(ModuleAService.class, new CachedServiceFetcher() 
           @Override
           public ModuleAService createService(@NotNull ModularManage manager) 
               if (service == null) 
                   service = new ModuleAServiceImpl();
               
               return service;
           
    

模块生命周期

模块有以下几个生命周期:

  • onInstalled() -> 模块被注册的时候调用:Module 在 App 中被注册的时候
  • onCreate() -> 模块第一次启动的时候调用:Module 所属的某个 Activity 第一次启动的时候
  • onStart() -> 模块启动的时候调用:模块第一次启动之的时候
  • onStop() -> 模块停止的时候调用:Activity 栈里面没有模块所属 Activity 的时候

模块生命周期的实现

其实组件内关于生命周期捕获和监听,都是借助于 Google 的 Android Architecture Components 中的 Lifecycle 库来实现的。

  • 模块生命周期的捕获:首先需要将 Activity 的类注册到 Module 中,然后全局监听 Activity 的 Lifecycle.Event事件,就可以获取到模块内 Activity 的运行情况
  • 模块生命周期的监听:BaseModule 本身继承了LifecycleOwner 接口,可以对其添加观察者,来实现对模块生命周期的监听

Modular-plugin

这里需要依赖对于 Android 的构建工具 Gralde 的扩展,它支持的高度可扩展特性,帮助我们在组件化开发中更加高效,不需要关系一些额外的工作,只需要关注开发的内容即可,对现有的代码逻辑基本没有侵入。

Gralde 的生命周期

这里必须要提一些的就是 Gradle 的生命周期,因为我们的很多扩展功能,都是在对 Gradle 执行的生命周期的各个阶段做一些改动来实现的,大概的生命周期如图:

单模块打包

Android 打包成 Apk 并运行的条件有:

  • AndroidManifest.xml 的配置支持(application 标签的配置)
  • 主 Activity 的配置

实现原理

  • 自动生成模块自己的 Application 类
  • 自动读取 Module 的 AndroidManifest 文件并修改成可以打包成 App 的配置
  • 在打包的时候动态更改 SourceSet,使打包的时候使用生成的文件进行打包
  • 在打包的时候动态更改支持的 Plugin 类(‘com.android.application’或是’com.android.library’)

修改模块 build.gradle 的配置

将以下配置添加到模块目录下的 build.gradle 文件中

modular 
        // 模块包名
        packageName = "com.youzan.ebizcore.plugin.demoa"

        app 
            // 单模块打包开关
                asApp = true
                // 运行的 App 的名称
                appName = "Module A"
                // 入口 Activity
                launchActivity = "com.youzan.ebizcore.plugin.demoa.ModuleAActivity"

                // 配置只在单模块打包时需要引入的依赖
                requires 
                    require "com.squareup.picasso:picasso:2.3.2"
                
            
        

生成单模块运行需要的环境

运行 modular 的 createApp Task,就会自动生成需要的类(以 module_a 为例)

自动生成的文件目录结构:
./module_a
    --src
    ----main
    ------app # 自动生成 app 目录
    --------java # 自动生成 Application 类
    --------res # 自动生成资源
    --------AndroidManifest.xml # 自动生成 Manifest 文件

执行单模块打包并安装的 Task

运行 modular 的 runAsApp Task,模块就会被单独达成一个 apk 包,并安装到你的手机上,如果模块有上下文依赖(比如登录)的话可以额外提供依赖,加到模块的 app 的 requires 中。 这里的打包执行是在 build 目录下生成了一个打包脚本,并调用 Gradle 的 API 执行脚本来实现打包安装的。

模块 API 管理

模块 API 层提供的接口和数据结构代码是可以直接在模块内部被引用到的,方便开发,但是在暴露给外部的模块时候的时候是需要打包成 aar 上传到 Maven 来提供的,Modular-Plugin 分别针对这两个步骤提供了两个 Task,方便开发者快速进行开发和发布。

在 build.gralde 中添加相关配置

modular 
        packageName = "com.youzan.ebizcore.plugin.demoa"
        // 模块 API 支持相关参数
        api 
              // 是否需要提供 API 支持的开关(会影响到是否可以运行自动生成代码的 Task)
              hasApi = true
              // 对外提供的 API Service 类名
              apiService = "ModuleAService"
              // API 层的依赖
              requires 
                  require "com.google.code.gson:gson:2.8.2"
              
         

生成 API 打包需要的文件

运行 modular 的 createApi Task,就会自动生成需要的类(以 module_b 为例)

./module_b
      --src
      ----main
      ------service # 自动生成 service 目录,用来存放对外接口和数据对象
      --------java # 自动生成 Application 类
      --------AndroidManifest.xml # 自动生成 Manifest 文件,为了单独打成 aar 包

模块发布

发布功能内部使用了 ‘maven-publish’ 插件来进行依赖的上传,开发者只关心上报的配置就好

在 build.gralde 中添加发布配置

  modular
         // 模块发布需要的参数
         publish 
             // 是否打开模块发布
             active = true
             // 上报地址,支持本地路径和远程 Mave 仓库地址
             repo = "../release"
             groupId = "com.youzan.ebizmobile.demo"
             artifactId = "modular-a"
             // 上报的业务模块 aar 包的版本号
             moduleVersion = "0.1.4"
             // 上报的 API 层 aar 包的版本号
             apiVersion = "0.1.5"
             // Maven 登录名和密码,可以从 local.properties 中取
             userName = ""
         
  

执行发布的 Task

运行 modular 的 uploadModule Task,Module-Plugin 会执行打包上传的任务,执行顺序是这样的:

  1. 首先打包并上传 Module 的 API 模块(SourceSet 只包含 API 的类)
  2. 将 Module API 的代码从模块的 SourceSet 中去除,并添加刚才上报的 API 模块的 Maven 依赖到 Module 的 dependencies 中

2.4.Modular-support

基础组件抽象

以图片组件为例,一般业务模块中使用到的图片相关的功能有:图片加载、图片选择等,可以把这些功能抽象成接口

  interface IImageLoadSupport 
      fun <IMAGE : ImageView> loadImage(imageView: IMAGE?, imgUrl: String)
      fun <IMAGE : ImageView> loadImage(imageView: IMAGE?, @DrawableRes drawableId: Int)
      fun <IMAGE : ImageView> loadImage(imageView: IMAGE?, imgUrl: String, callback:ImageLoadCallback<IMAGE>)
      fun imagePicker(activity: Activity?, selectedImgUris: List<Uri>)
      fun onImagePickerResult(requestCode: Int, resultCode: Int, intent: Intent?):List<String>?
  

基础组件的实现

基础组件的实现可以在 App 中进行注册,如果需要单模块组件中使用 Support 相关功能,可以提供一套默认实现,在但模块运行时引入,在全局有一个 Support 注册中心,以 Map 的形式维护运行中的 Support 对象:

fun <SUPPORT : Any, SUPPORTIMPL : SUPPORT> registerProvider(supportCls: Class<SUPPORT>,provider: SupportProvider<SUPPORTIMPL>) 
    synchronized(Lock) 
        supportsProviderMap[supportCls] = provider
        if (supportsMap.containsKey(supportCls)) 
            supportsMap.remove(supportCls)
        
    

规划

开发到现在,这边的三个组件已经能够基本完成我们对于组件化核心需求,但是,也是有一些方向可以进一步优化整套方案的使用:

  • Modular-Support 组件引入依赖注入的方式实现 API 的调用,使用方可以不再需要关心实例对象的获取
  • Modular-Support 组件可以提供给 Weex、RN、H5、Flutter 业务一些原生的功能
  • Modular-Plugin 能够进一步压缩打包时间,并且让开发中的依赖配置更加灵活
  • Modular-Plugin 继续优化管理和依赖打包的功能,提高效率

关注公众号:初一十五a
解锁 《Android十三大板块文档》,让学习更贴近未来实战。已形成PDF版

内容如下

1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试应有尽有
3.Android车载应用大合集,从零开始一起学
4.性能优化大合集,告别优化烦恼
5.Framework大合集,从里到外分析的明明白白
6.Flutter大合集,进阶Flutter高级工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对工作需求
10.Android基础篇大合集,根基稳固高楼平地起
11.Flutter番外篇:Flutter面试+Flutter项目实战+Flutter电子书
12.高级Android组件化强化实战
13.十二模块之补充部分:其他Android十一大知识体系

整理不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔

以上是关于从大厂APP看Android组件化实践最终篇的主要内容,如果未能解决你的问题,请参考以下文章

关于Android组件化的深度分析篇大厂架构

Android 组件化从入门篇到大厂(微信支付宝爱奇艺携程美团....)强化实战篇,Android 进阶必备

Android自己定义组件系列——进阶实践

音视频大合集最终篇;学废了

Android插件化方案实践

Android插件化方案实践