IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 美团组件路由原理与实现详解,这个回答让我错失offer -> 正文阅读

[移动开发]美团组件路由原理与实现详解,这个回答让我错失offer

学到老活到老,路漫漫其修远兮。与众君共勉 !


引子

最近得高人指点,恰巧工作中做过一个移植其他app的某个功能模块的任务,过程简直痛不欲生。
然后思考如何对app的各个功能模块进行灵活拔插配置,最大程度减少移植代码出错的可能性,保证功能模块的完整移植*

此次手写架构,解决的问题是:

1、让 App内 各个功能模块能够独立开发单元测试,也可以 所有模块集成打包,统一测试

独立开发

更改gradle.properties的配置,使得每个功能模块都成为application, 可以独立打包成apk,单独运行。单个模块,独立测试。

集成打包

更改gradle.properties的配置,使得原先每个单独模块,都变成library,被 主模块引用,这时候只有主模块能够打包apk,所有功能都集成在这个apk内。

2、实现 功能模块的整体移植,灵活拔插

故事背景

当你们公司有多个安卓开发人员,开发出核心业务相同,但是UI不同,其他业务不同的一系列App时(如果核心业务是X,你们有5个开发人员,做出了A,B,C,D,E 5个app,都包含核心业务X,但是除了X之外,其他的业务模块各不相同)这时候,如果领导要把A里面的一个非核心功能,挪到B里面…

现状

开发B的程序猿可能要骂娘,因为他在从移植A的代码中剥离代码 遇到了很多高耦合,低内聚 的类结构,挪过来之后,牵一发而动全身,动一点小地方,整个代码满江红。

理想

如果这个时候,我们通过代码框架的配置,能够把A里面的一个模块,作为一个module 移植到 工程内部,然后主module 来引用这个module,略微写一些代码来使得这个功能模块在app中生效。那么无论是多少个功能模块,都可以作为整体来 给其他app复用。这样开发人员也不用相互骂娘了,如果挪过来的模块存在bug或者其他问题,也不用甩锅,模块原本是谁开发的,找谁就好了。

3、保证App内 业务模块的相互隔离,但是又不妨碍业务模块之间的数据交互

我们开发app的功能模块,一个业务,可能是通过一个Activity或者 一个Fragment 作为对外的窗口,也可能是。**所谓窗口,就是这个业务,相对于其他模块,"有且只有"一个入口,没有任何其他可以触达到这个业务的途径。***业务代码之间相互隔离,绝对不可以有相互引用。那么,既然相互不会引用,那A模块一定要用到B模块的数据,怎么办呢?下文提供解决方案。

正文大纲

1、代码结构现状以及理想状态一览

2、功能组件化的实现思路,实现组件移植拔插

3、参考ARouter源码,写出自己的Router框架,统一通过Router来进行模块的切换 以及 组件之间数据的交互

4、使用组件api化,在模块很多的情况下优化公共模块的结构

正文

1、代码结构现状以及理想状态一览

先来看两张图

现状

代码有模块化的迹象,但是没有对业务模块进行非常明显的模块化**(不明白啥意思是吧?不明白就对了,app这个module里面其实还有很多东西没有展示出来,请看下图:试想,把所有的模块集中到一个module的一个包里面,当你要移植某一个功能的时候,想想那酸爽…当然如果你口味别致,那当我没说)**

理想:

理想化的话,参照:理想.png; 项目结构层次分明,脉络清晰

按照图中的分层,详细解释一下:

外壳层:app module

内部代码只写 app的骨骼框架,比如说,你的app是这个样子的结构:

下方有N个TAB,通过Fragment来进行切换模块。这种架构肯定不少见。

这个时候,外壳层 app module,就只需要写上 上面这种UI架构的框架代码就行了,至于有多少个模块,需要代码去读取配置进行显示。神马?你问我怎么写这种UI框架?网上一大把,如果实在找不到,来我的 github项目地址
?乛?乛?

业务层

我们的业务模块,对外接口可能是一个Activity* *(**比如说,登录模块,只对外提供一个LoginActivity,有且仅有这一个窗口)或者 是一个Fragment,就像上图(典型的app架构.png), 如果app的UI框架是通过切换Fragment来却换业务模块的话。business*这个目录,将所有的业务模块包含进去,每个模块又是独立的module,这样既实现了业务代码隔离,又能一眼看到所有的业务模块,正所谓,一目了然。

功能组件层

每一个业务模块,不可避免的需要用到一些公用工具类,有的是第三方SDK的再次封装,有的是自己的工具类,或者自己写的自定义控件,还有可能是 所有业务模块都需要的 辅助模块,都放在这里。

路由框架层

设计这一层,是想让app内的所有Activity,业务模块Fragment,以及模块之间的数据交互,都由 这一层开放出去的接口来负责

gradle统一配置文件

工程内部的一些全局gradle变量,放在这里,整个工程都有效

module编译设置

setting.gradle 配置要编译的module; 也可以做更复杂的操作,比如,写gradle代码去自动生成一些module,免除人为创建的麻烦.

2. 功能组件化的实现思路,实现组件移植拔插

能够兼顾 每个模块的单独开发,单独测试 和 整体打包,统一测试。 听起来很神奇的样子,但是其实就一个核心:gradle编程。

打开gradle.properties文件:

注解应该很清晰了,通过一个全局变量,就可以控制当前是要 模块化单元测试呢?还是要集成打包apk测试。

*那么,只写一个isModule就完事了吗?当然不是,还有*一堆杂事 需要我们处理,我们要使用这个全局变量。

一堆杂事,分为两类:

1- app 外壳层module 的build.gradle(注意:写在dependencies)

if (isModule.toBoolean()) {
implementation project(":business:activity_XXX")
//…在这里引用更多业务模块
}

2- 每个业务module的build.gradle

第一处:判定 isModule,决定当前module是要当成library还是application

if (isModule.toBoolean()) {
apply plugin:‘com.android.library’
} else {
apply plugin:‘com.android.application’*
}

第二处:更改defaultConfig里面的部分代码,为什么要改?因为当当前module作为library的时候,不能有applicationId "XXXX"这一句

defaultConfig {
if (!isModule.toBoolean()) {
applicationId"study.hank.com.XXXX"*
}

}

sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile ‘src/main/module/AndroidManifest.xml’
} else {
manifest.srcFile ‘src/main/AndroidManifest.xml’
}
}
}

由于要区分对待,我们就需要另外创建一个manifest文件,移除launcher配置即可。参考下图:

这就是业务模块组件化的秘密了。

什么,你问我怎么 进行功能拔插?

当你不需要某一个模块的时候,

1)在app的build.gradle里面 把 引用该模块的配置去掉;

2)setting.gradle 的include 去掉它

3)app module 里面,改动代码,不再使用这个模块。(这个我就不截图了,因为app module的UI框架代码不是一句话说得清的。请运行我的demo源码自己看吧)

功能的插入,同理,上面的过程倒过来走一遍,就不浪费篇幅了。


3. 参考ARouter源码,写出自己的Router框架,统一通过Router来进行模块的切换 以及组件之间数据的交互

说到路由框架的使用价值,两点:

1、在app实现了组件化之后,由于组件之间由于代码隔离,不允许相互引用,导致 相互不能直接沟通,那么,就需要一个 “中间人角色” 来帮忙*" **带话"***了.

2、app内部,不可避免地要进行Activity跳转,Fragment切换。把这些重复性的代码,都统一让路由来做吧。省了不少代码行数。

阅读了阿里巴巴ARouter的源码,参照阿里大神的主要思路,简化了一些流程,去掉了一些我不需要的功能,增加了一些我独有的功能,加入了一些自己的想法,写出了自己的 ZRouter 路由 框架。那就不罗嗦了,上干货。

基础知识

如果以下基础知识不具备,建议先去学习基础知识。 或者 也可以跟着笔者的思路来看代码,慢慢理解这些知识的实用价值。

java反射机制(路由框架里大量地使用了 class反射创建 对象)

APT 注解,注解解析机制(注解解析机制贯穿了整个路由框架)

javapoet , java类的元素结构(一些人为写起来很麻烦的代码,一些脏活累活,就通过自动生成代码来解决)

如何使用

1- 在app module的自定义Application类里面,进行初始化, ZRouter准备就绪

public class FTApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ZRouter.getInstance().initRegister(this);
}
}

**2- **就绪之后才可以直接使用(RouterPathConst 里面都是我自己定义的String常量):

切换Fragment*

ZRouter.getInstance().build(RouterPathConst.PATH_FRAGMENT_MINE).navigation();

跳转Activity

ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();

组件之间的通信,取得Mine模块的 accountNo 然后 toast出来

String accountNo = ZRouter.getInstance().navigation(MineOpenServiceApi.class).accountNo();

Toast.makeText(getActivity(), accountNo, Toast.LENGTH_LONG).show();

如我们之前所设想的,切换Fragment,跳转Activity,组件之间的通信 全部只能通过 ZRouter框架来执行。

3- 退出app时,要释放ARouer的资源(主要是静态变量)

ZRouter.getInstance().release();

4- 每个业务模块,在将要暴露出去的Fragment或者Activity上,要加上注解

@ZRoute(RouterPathConst.PATH_ACTIVITY_CHART)//注册Activity
public class ChartActivity extends AppCompatActivity {···}

或者

@ZRoute(RouterPathConst.PATH_FRAGMENT_HOME)//注册Fragment
public class HomeFragment extends Fragment {···}

或者

@ZRoute(RouterPathConst.PATH_PROVIDER_MINE)
// 注册数据接口
public class MineServiceImpl implements MineOpenServiceApi {···}

设计思路

讲解设计思路,必须用源码进行参照,请务必参照源码 。
源码地址为:https://github.com/18598925736/EvolutionPro

说明一下本人 阅读源码的方法。也许很多人都和曾经的我一样,看到一份第三方SDK的源码,不知道从何下手,要么看了半天还在原地打转转,要么就是这里看一点,那里看一点,没有中心思想,看了半天毫无收获。

干货:看源码要思路清晰,目的明确。一切技术的价值都只有一个,那就是解决实际问题。既然是解决实际问题,那我们就从这个SDK暴露出来的最外围接口为起点,看这个接口的作用是什么,解决了什么问题,顺藤摸瓜,找找它解决问题的核心方法,至于顺藤摸瓜道路上遇到的枝枝脉脉,要分清哪些是辅助类(每个人写辅助类的习惯可能都不同,所以不必太在意),哪些是核心类(核心思想一般都是大同小异)。找到了核心思想,再从头重新过几遍,SDK的设计思路就了然于胸了.

按照我的上面提供的“干货”,如果你现在下载了我的Demo源码,那么我们继续:

如果把看源码的结构,理解为 警察查案。那么就要从最表层的现象开始着手,慢慢查找根源。

HomeFragment.java的54行, 这里要进行Activity跳转。

ZRouter.getInstance().build(RouterPathConst.PATH_ACTIVITY_CHART).navigation();

这里有getInstance()方法,build()方法,还有navigation()方法,一个一个看

  • getInstance()是处在ZRouter类内部,是ZRouter的单例模式的get方法,单例模式就不赘述了,我写了注释
  • build()方法也是在ZRouter类内部,逻辑很简单,就是new Postcard(path) 参数path是一个string,方法返回值是一个Postcard对象
  • navigation()方法是在Postcard类内部,但是,具体的执行逻辑,依然是在ZRouter类里面
    getInstance()build()方法都很简单,不需要花太多精力。下面继续跟随ZRouternavigation()方法“追查”

ZRouternavigation() 方法内容如下:

Object navigation(Postcard postcard) {
LogisticsCenter.complete(postcard);
switch (postcard.getRouteType()) {
case ACTIVITY://如果是Activity,那就跳吧
return startActivity(postcard);
case FRAGMENT://如果是Fragment,那就切换吧
return switchFragment(postcard);
case PROVIDER://如果是Provider,那就执行业务逻辑
return postcard.getProvider();//那就直接返回provider对象
default:
break;
}
return null;
}

发现一个可疑的代码:LogisticsCenter.complete(postcard); 看方法名,应该是对postcard对象进行完善。
进去追查

/**

  • Postcard字段补全
  • @param postcard
    */
    public static void complete(Postcard postcard) {
    if (null == postcard) {
    throw new RuntimeException(“err:postcard 是空的,怎么搞的?”);
    }

RouteMeta routeMeta = Warehouse.routeMap.get(postcard.getPath());//
if (null == routeMeta) {//如果路由meta是空,说明可能这个路由没注册,也有可能路由表没有去加载到内存中
throw new RuntimeException(“err:路由寻址失败,请检查是否path写错了”);
} else {
postcard.setDestination(routeMeta.getDestination());
postcard.setRouteType(routeMeta.getRouteType());

···
}
}

这段代码,从一个map中,用path作为keyget出了一个RouteMeat对象,然后用这个对象的字段值,对参数postcard的属性进行赋值。好像有点莫名其妙。看不太懂。不着急,继续。

刚才的navigation()方法这里存在switch分支,分支设计到ACTIVITY,FRAGMENT,PROVIDER,由于我们这次追查的只是activity相关,所以,忽略掉其他分支,只追查startActivity(postcard); 下面是该方法的代码:

private Object startActivity(Postcard postcard) {
Class<?> cls = postcard.getDestination();
if (cls == null) {
if (cls == null)
throw new RuntimeException(“没找到对应的activity,请检查路由寻址标识是否写错”);
}
final Intent intent = new Intent(mContext, cls);
if (Postcard.FLAG_DEFAULT != postcard.getFlag()) {//如果不是初始值,也就是说,flag值被更改过,那就用更改后的值
intent.setFlags(postcard.getFlag());
} else {//如果沒有设定启动模式,即 flag值没有被更改,就用常规模式启动
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//常规模式启动Activity
}
//跳转只能在主线程中进行
runInMainThread(new Runnable() {
@Override
public void run() {
mContext.startActivity(intent);
}
});
return null;
}

这里只是一个简单的跳转操作,但是,发现了一个关键点,跳转的“目的地”class是来自 postcarddestination . 发现规律了,原来刚才在 LogisticsCenter.complete(postcard); 里面进行postcard“完善”的时候,set进去的destination 原来在这里被使用到。

那么问题的关键点就发生了转移了, 这个destination Class是从map里面get出来的,那么,又是什么时候被put进去的呢?
开始追踪这个map : Warehouse.routeMap ,通过代码追踪,可以发现,唯一可能往map里面put东西的代码只有这一句:

/**

  • 反射执行APT注册文件的注册方法
    */
    private static void registerComm() {
    try {
    Set classNames = ClassUtils.getFileNameByPackageName(mContext, RouterConst.GENERATION_PACKAGE_NAME);//找到包名下的所有class
    for (String className : classNames) {
    Class<?> clz = Class.forName(className);
    if (IRouterZ.class.isAssignableFrom(clz)) {
    IRouterZ iRouterComm = (IRouterZ) clz.getConstructor().newInstance();
    iRouterComm.onLoad(Warehouse.routeMap);
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    classNames = ClassUtils.getFileNameByPackageName(mContext, RouterConst.GENERATION_PACKAGE_NAME);//找到包名下的所有class
    for (String className : classNames) {
    Class<?> clz = Class.forName(className);
    if (IRouterZ.class.isAssignableFrom(clz)) {
    IRouterZ iRouterComm = (IRouterZ) clz.getConstructor().newInstance();
    iRouterComm.onLoad(Warehouse.routeMap);
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-01-24 11:01:03  更:2022-01-24 11:01:46 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 12:52:11-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码