面试官: 有没有使用过组件化, 组件化通信如何做到的, ARouter 有用过吗
心理分析: 组件化一般在架构经常被考到, 组件化的内容比较多, 跟我们平时的开发 mvc 的单体应用不一样, 组件化是团队作战, 需要设计复杂的组件通信与交互
求职者: 从组件化的由来, 优势 弊端开始, 最后引出组件化的劣势, 组件通信
1 组件化
1.1 组件化初衷
App 版本不断的迭代, 新功能的不断增加, 业务也会变的越来越复杂, 维护成本高.
业务耦合度高, 代码越来越臃肿, 团队内部多人协作开发困难.
Android 项目在编译代码的时候电脑会非常卡, 又因为单一工程下代码耦合严重, 每修改一处代码后都要重新编译打包测试, 导致非常耗时.
方便单元测试, 改动单独一个业务模块, 不需要着重于关注其他模块被影响.
1.2 什么是组件化
组件化就是将一个 App 分成多个 Module, 如下图, 每个 Module 都是一个组件 (也可以是一个基础库供组件依赖), 开发的过程中我们可以单独调试部分组件, 组件间不需要互相依赖, 但可以相互调用, 最终发布的时候所有组件以 lib 的形式被主 App 工程依赖并打包成一个 apk.
1.3 组件化优势
组件化就是将通用模块独立出来, 统一管理, 以提高复用, 将页面拆分为粒度更小的组件, 组件内部除了包含 UI 实现, 还包含数据层和逻辑层.
每个工程都可以独立编译, 加快编译速度, 独立打包.
每个工程内部的修改, 不会影响其他工程.
业务库工程可以快速拆分出来, 集成到其他 App 中.
迭代频繁的业务模块采用组件方式, 业务线研发可以互不干扰, 提升协作效率, 并控制产品质量, 加强稳定性.
并行开发, 团队成员只关注自己的开发的小模块, 降低耦合性, 后期维护方便等.
2 组件化通信
2.1 组件化通信
组件化互相不直接依赖, 如果组件 A 想调用组件 B 的方法是不行的. 很多开发者因为组件化之间通信比较复杂 则放弃了组件化的使用
组件通信有以下几种方式:
1. 本地广播
本地广播, 也就是 LoacalBroadcastRecevier. 更多是用在同一个应用内的不同系统规定的组件进行通信, 好处在于: 发送的广播只会在自己的 App 内传播, 不会泄漏给其他的 App, 其他 App 无法向自己的 App 发送广播, 不用被其他 App 干扰. 本地广播好比对讲通信, 成本低, 效率高, 但有个缺点就是两者通信机制全部委托与系统负责, 我们无法干预传输途中的任何步骤, 不可控制, 一般在组件化通信过程中采用比例不高.
2. 进程间的 AIDL
进程间的 AIDL. 这个粒度在于进程, 而我们组件化通信过程往往是在线程中, 况且 AIDL 通信也是属于系统级通信, 底层以 Binder 机制, 虽说 Android 提供模板供我们实现, 但往往使用者不好理解, 交互比较复杂, 往往也不适用应用于组件化通信过程中.
3. 匿名的内存共享
匿名的内存共享. 比如用 Sharedpreferences, 在处于多线程场景下, 往往会线程不安全, 这种更多是存储一一些变化很少的信息, 比如说组件里的配置信息等等
4.Intent Bundle 传递
Intent Bundle 传递. 包括显性和隐性传递, 显性传递需要明确包名路径, 组件与组件往往是需要互相依赖, 这背离组件化中 SOP(关注点分离原则), 如果走隐性的话, 不仅包名路径不能重复, 需要定义一套规则, 只有一个包名路径出错, 排查起来也稍显麻烦, 这个方式往往在组件间内部传递会比较合适, 组件外与其他组件打交道则使用场景不多.
2.2 目前主流做法之一就是引入第三者, 比如图中的 Base Module.
image
3 ARouter 组件通信框架
3.1 ARouter 简介
是 ARouter 是阿里巴巴开源的 Android 平台中对页面, 服务提供路由功能的中间件, 提倡的是简单且够用. 主要用作组件化通信
GitHub:https://github.com/alibaba/ARouter
前言
- Intent intent = new Intent(mContext, XxxActivity.class);
- intent.putExtra("key","value");
- startActivity(intent);
- Intent intent = new Intent(mContext, XxxActivity.class);
- intent.putExtra("key","value");
- startActivityForResult(intent, 666);
上面一段代码, 在 Android 开发中, 最常见也是最常用的功能就是页面的跳转, 我们经常需要面对从浏览器或者其他 App 跳转到自己 App 中页面的需求, 不过就算是简简单单的页面跳转, 随着时间的推移, 也会遇到一些问题:
集中式的 URL 管理: 谈到集中式的管理, 总是比较蛋疼, 多人协同开发的时候, 大家都去 AndroidManifest.xml 中定义各种 IntentFilter, 使用隐式 Intent, 最终发现 AndroidManifest.xml 中充斥着各种 Schame, 各种 Path, 需要经常解决 Path 重叠覆盖, 过多的 Activity 被导出, 引发安全风险等问题
可配置性较差: Manifest 限制于 xml 格式, 书写麻烦, 配置复杂, 可以自定义的东西也较少
跳转过程中无法插手: 直接通过 Intent 的方式跳转, 跳转过程开发者无法干预, 一些面向切面的事情难以实施, 比方说登录, 埋点这种非常通用的逻辑, 在每个子页面中判断又很不合理, 毕竟 activity 已经实例化了
跨模块无法显式依赖: 在 App 小有规模的时候, 我们会对 App 做水平拆分, 按照业务拆分成多个子模块, 之间完全解耦, 通过打包流程控制 App 功能, 这样方便应对大团队多人协作, 互相逻辑不干扰, 这时候只能依赖隐式 Intent 跳转, 书写麻烦, 成功与否难以控制.
为了解决以上问题, 我们需要一款能够解耦, 简单, 功能多, 定制性较强, 支持拦截逻辑的路由组件: 我们选择了 Alibaba 的 ARouter, 偷个懒, 直接贴 ARouter 的中文介绍文档:
3.2 ARouter 优势
从 ARouter GitHub 了解到它的优势:
支持直接解析标准 URL 进行跳转, 并自动注入参数到目标页面中 支持多模块工程使用 支持添加多个拦截器, 自定义拦截顺序 支持依赖注入, 可单独作为依赖注入框架使用 支持 InstantRun 支持 MultiDex(Google 方案) 映射关系按组分类, 多级管理, 按需初始化 支持用户指定全局降级与局部降级策略 页面, 拦截器, 服务等组件均自动注册到框架 支持多种方式配置转场动画 支持获取 Fragment 完全支持 Kotlin 以及混编 典型的应用:
从外部 URL 映射到内部页面, 以及参数传递与解析 跨模块页面跳转, 模块间解耦 拦截跳转过程, 处理登陆, 埋点等逻辑
跨模块 API 调用, 通过控制反转来做组件解耦
三, 典型应用场景
从外部 URL 映射到内部页面, 以及参数传递与解析
跨模块页面跳转, 模块间解耦
拦截跳转过程, 处理登陆, 埋点等逻辑
跨模块 API 调用, 模块间解耦 (注册 ARouter 服务的形式, 通过接口互相调用)
四, 基础功能
添加依赖和配置
- apply plugin: 'com.neenbedankt.android-apt'
- buildscript {
- repositories {
- jcenter()
- }
- dependencies {
- classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
- }
- }
- apt {
- arguments {
- moduleName project.getName();
- }
- }
- dependencies {
- apt 'com.alibaba:arouter-compiler:x.x.x'
- compile 'com.alibaba:arouter-api:x.x.x'
- ...
- }
添加注解
- // 在支持路由的页面, 服务上添加注解 (必选)
- // 这是最小化配置, 后面有详细配置
- @Route(path = "/test/1")
- public class YourActivity extend Activity {
- ...
- }
初始化 SDK
ARouter.init(mApplication); // 尽可能早, 推荐在 Application 中初始化
发起路由操作 // 1. 应用内简单的跳转 (通过 URL 跳转在'中阶使用'中) ARouter.getInstance().build("/test/1").navigation();
- // 2. 跳转并携带参数
- ARouter.getInstance().build("/test/1")
- .withLong("key1", 666L)
- .withString("key3", "888")
- .navigation();
添加混淆规则 (如果使用了 Proguard)
-keep public class com.alibaba.Android.arouter.routes.**{*;}
五, 进阶用法
通过 URL 跳转
- // 新建一个 Activity 用于监听 Schame 事件
- // 监听到 Schame 事件之后直接传递给 ARouter 即可
- // 也可以做一些自定义玩法, 比方说改改 URL 之类的
- // http://www.example.com/test/1
- public class SchameFilterActivity extends Activity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- // 外面用户点击的 URL
- Uri uri = getIntent().getData();
- // 直接传递给 ARouter 即可
- ARouter.getInstance().build(uri).navigation();
- finish();
- }
- }
- // AndroidManifest.xml 中 的参考配置
- <activity Android:name=".activity.SchameFilterActivity">
- <!-- Schame -->
- <intent-filter>
- <data
- Android:host="m.aliyun.com"
- Android:scheme="arouter"/>
- <action Android:name="android.intent.action.VIEW"/>
- <category Android:name="android.intent.category.DEFAULT"/>
- <category Android:name="android.intent.category.BROWSABLE"/>
- </intent-filter>
- <!-- App Links -->
- <intent-filter Android:autoVerify="true">
- <action Android:name="android.intent.action.VIEW"/>
- <category Android:name="android.intent.category.DEFAULT"/>
- <category Android:name="android.intent.category.BROWSABLE"/>
- <data
- Android:host="m.aliyun.com"
- Android:scheme="http"/>
- <data
- Android:host="m.aliyun.com"
- Android:scheme="https"/>
- </intent-filter>
- </activity>
使用 ARouter 协助解析参数类型
- // URL 中的参数会默认以 String 的形式保存在 Bundle 中
- // 如果希望 ARouter 协助解析参数 (按照不同类型保存进 Bundle 中)
- // 只需要在需要解析的参数上添加 @Param 注解
- @Route(path = "/test/1")
- public class Test1Activity extends Activity {
- @Param // 声明之后, ARouter 会从 URL 中解析对应名字的参数, 并按照类型存入 Bundle
- public String name;
- @Param
- private int age;
- @Param(name = "girl") // 可以通过 name 来映射 URL 中的不同参数
- private boolean boy;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- name = getIntent().getStringExtra("name");
- age = getIntent().getIntExtra("age", -1);
- boy = getIntent().getBooleanExtra("girl", false); // 注意: 使用映射之后, 要从 Girl 中获取, 而不是 boy
- }
- }
开启 ARouter 参数自动注入 (实验性功能, 不建议使用, 正在开发保护策略)
- // 首先在 Application 中重写 attachBaseContext 方法, 并加入 ARouter.attachBaseContext();
- @Override
- protected void attachBaseContext(Context base) {
- super.attachBaseContext(base);
- ARouter.attachBaseContext();
- }
- // 设置 ARouter 的时候, 开启自动注入
- ARouter.enableAutoInject();
- // 至此, Activity 中的属性, 将会由 ARouter 自动注入, 无需 getIntent().getStringExtra("xxx") 等等
声明拦截器 (拦截跳转过程, 面向切面搞事情)
- // 比较经典的应用就是在跳转过程中处理登陆事件, 这样就不需要在目标页重复做登陆检查
- // 拦截器会在跳转之间执行, 多个拦截器会按优先级顺序依次执行
- @Interceptor(priority = 666, name = "测试用拦截器")
- public class TestInterceptor implements IInterceptor {
- /**
- * The operation of this interceptor.
- *
- * @param postcard meta
- * @param callback cb
- */
- @Override
- public void process(Postcard postcard, InterceptorCallback callback) {
- ...
- callback.onContinue(postcard); // 处理完成, 交还控制权
- // callback.onInterrupt(new RuntimeException("我觉得有点异常")); // 觉得有问题, 中断路由流程
- // 以上两种至少需要调用其中一种, 否则会超时跳过
- }
- /**
- * Do your init work in this method, it well be call when processor has been load.
- *
- * @param context ctx
- */
- @Override
- public void init(Context context) {
- }
- }
处理跳转结果
- // 通过两个参数的 navigation 方法, 可以获取单次跳转的结果
- ARouter.getInstance().build("/test/1").navigation(this, new NavigationCallback() {
- @Override
- public void onFound(Postcard postcard) {
- ...
- }
- @Override
- public void onLost(Postcard postcard) {
- ...
- }
- });
自定义全局降级策略
- // 实现 DegradeService 接口, 并加上一个 Path 内容任意的注解即可
- @Route(path = "/xxx/xxx") // 必须标明注解
- public class DegradeServiceImpl implements DegradeService {
- /**
- * Router has lost.
- *
- * @param postcard meta
- */
- @Override
- public void onLost(Context context, Postcard postcard) {
- // do something.
- }
- /**
- * Do your init work in this method, it well be call when processor has been load.
- *
- * @param context ctx
- */
- @Override
- public void init(Context context) {
- }
- }
为目标页面声明更多信息
- // 我们经常需要在目标页面中配置一些属性, 比方说 "是否需要登陆" 之类的
- // 可以通过 Route 注解中的 extras 属性进行扩展, 这个属性是一个 int 值, 换句话说, 单个 int 有 4 字节, 也就是 32 位, 可以配置 32 个开关
- // 剩下的可以自行发挥, 通过字节操作可以标识 32 个开关
- @Route(path = "/test/1", extras = Consts.XXXX)
使用 ARouter 管理服务 (一) 暴露服务
- /**
- * 声明接口
- */
- public interface IService extends IProvider {
- String hello(String name);
- }
- /**
- * 实现接口
- */
- @Route(path = "/service/1", name = "测试服务")
- public class ServiceImpl implements IService {
- @Override
- public String hello(String name) {
- return "hello," + name;
- }
- /**
- * Do your init work in this method, it well be call when processor has been load.
- *
- * @param context ctx
- */
- @Override
- public void init(Context context) {
- }
- }
使用 ARouter 管理服务 (二) 发现服务
1. 可以通过两种 API 来获取 Service, 分别是 ByName,ByType
- IService service = ARouter.getInstance().navigation(IService.class); // ByType
- IService service = (IService) ARouter.getInstance().build("/service/1").navigation(); // ByName
- service.hello("zz");
2. 注意: 推荐使用 ByName 方式获取 Service,ByType 这种方式写起来比较方便, 但如果存在多实现的情况时, SDK 不保证能获取到你想要的实现
使用 ARouter 管理服务 (三) 管理依赖
可以通过 ARouter service 包装您的业务逻辑或者 sdk, 在 service 的 init 方法中初始化您的 sdk, 不同的 sdk 使用 ARouter 的 service 进行调用,
每一个 service 在第一次使用的时候会被初始化, 即调用 init 方法.
这样就可以告别各种乱七八糟的依赖关系的梳理, 只要能调用到这个 service, 那么这个 service 中所包含的 sdk 等就已经被初始化过了, 完全不需要
关心各个 sdk 的初始化顺序.
六, 更多功能
初始化中的其他设置
- ARouter.openLog(); // 开启日志
- ARouter.printStackTrace(); // 打印日志的时候打印线程堆栈
详细的 API 说明
- // 构建标准的路由请求
- ARouter.getInstance().build("/home/main").navigation();
- // 构建标准的路由请求, 并指定分组
- ARouter.getInstance().build("/home/main", "ap").navigation();
- // 构建标准的路由请求, 通过 Uri 直接解析
- Uri uri;
- ARouter.getInstance().build(uri).navigation();
- // 构建标准的路由请求, startActivityForResult
- // navigation 的第一个参数必须是 Activity, 第二个参数则是 RequestCode
- ARouter.getInstance().build("/home/main", "ap").navigation(this, 5);
- // 直接传递 Bundle
- Bundle params = new Bundle();
- ARouter.getInstance()
- .build("/home/main")
- .with(params)
- .navigation();
- // 指定 Flag
- ARouter.getInstance()
- .build("/home/main")
- .withFlags();
- .navigation();
- // 觉得接口不够多, 可以直接拿出 Bundle 赋值
- ARouter.getInstance()
- .build("/home/main")
- .getExtra();
- // 使用绿色通道 (跳过所有的拦截器)
- ARouter.getInstance().build("/home/main").greenChannal().navigation();
最后
给大家分享一份移动架构大纲, 包含了移动架构师需要掌握的所有的技术体系, 大家可以对比一下自己不足或者欠缺的地方有方向的去学习提升;
来源: http://www.jianshu.com/p/92bf4a16f40a