0%

组件化技术学习


随着Android技术发展越来越成熟,随着热更新技术的火热,组件化和插件化的技术也得到了一定的关注。特别是对于比较大的项目,人员的增多,代码越来越臃肿,这时候就必须进行模块化的拆分。模块化在Android工程中目前有两种实现方式,一种是组件化,另一种是插件化,其实质都是降低耦合。作为Android开发者,有必要对这两种技术做一个了解,在项目需要重构时或在构建大项目初期时,选择合适的模块化方案,下面就对组件化技术学习做一个总结。目前比较流行的组件化方案是阿里的Atlas,得到app的AndroidComponent,插件化比较流行的方案是滴滴的virtual apk,360的replugin。


得到团队的AndroidComponent的学习成本较低,对于初学者方便入手,以下记录该框架的学习情况:
每个组件都可以看成一个单独的整体,可以按需的和其他组件(包括主项目)整合在一起,从而完成的形成一个app,整体app缺少任何一个组件都是可以正常运行的,并且每个组件可以单独运行。
件化和插件化的最大区别(应该也是唯一区别)就是组件化在运行时不具备动态添加和修改组件的功能,但是插件化是可以的。

依赖库与Component的区别

依赖库library

代码被其他组件直接引用。比如网络库module可以认为是一个library。

Component

这种module是一个完整的功能模块。比如分享module就是一个Component。
统一把library称之为依赖库,把Component称之为组件。组件化也主要是针对Component这种类型。主项目、主module或者Host负责拼装这些组件以形成一个完成app的module,统一称为主项目。

项目结构

app是主项目,负责集成众多组件,控制组件的生命周期
xxxComponent 是我们拆分的两个组件,比如shareComponent
componentservice 定义了所有的组件提供的服务
basicres 定义了全局通用的theme和color等公共资源
basiclib 公共的基础库,统一在这里引入,比如一些第三方的库okhttp
componentlib 组件化的基础库 Router/UIRouter等都定义在这里
build-gradle 组件化编译的gradle插件

单独调试和发布

  1. 在组件工程下的gradle.properties文件中设置一个isRunAlone的变量来区分不同的场景,build.gradle需要引入一个插件,插件中会判断apply com.android.library还是com.android.application
  2. 在src/main/runalone下面定义单独调试所必须的AndroidManifest.xml、application、入口activity等类
  3. 组件开发并测试完成时,需要发布一个release版本的aar文件到中央仓库,只需要把isRunAlone修改为false,执行module:assembleRelease命令。只有发布组件时才需要修改isRunAlone=false,即使后面将组件集成到app中,也不需要修改isRunAlone的值。所以在Androidstudio中,是可以看到三个application工程的,随便点击一个都是可以独立运行的,并且可以根据配置引入其他需要依赖的组件,这些工作都由插件来完成。

组件交互

组件间数据传输,通过接口+实现的方式,组件之间完全面向接口编程。
下面是share提供一个fragment给app使用的例子。
share组件在componentservice中定义自己的服务

1
2
3
public interface ShareService {
Fragment getShareFragment();
}

在自己的组件工程中,提供对应的实现类ShareServiceImpl:

1
2
3
4
5
6
public class ShareServiceImpl implements ShareService {
@Override
public Fragment getShareFragment() {
return new ShareFragment();
}
}

在ShareAppLike中在组件加载的时候把实现类注册到Router中,ShareAppLike相当于组件的application类需要实现IApplicationLike的接口,在IApplicationLike接口中定义onCreate和onStop两个生命周期方法,对应组件的加载和卸载。

1
2
3
4
5
6
7
8
9
10
11
public class ShareAppLike implements IApplicationLike {
Router router = Router.getInstance();
@Override
public void onCreate() {
router.addService(ShareService.class.getSimpleName(), new ShareServiceImpl());
}
@Override
public void onStop() {
router.removeService(ShareService.class.getSimpleName());
}
}

在app中面向ShareService来编程

1
2
3
4
5
6
7
Router router = Router.getInstance();
if (router.getService(ShareService.class.getSimpleName()) != null) {
ShareService service = (ShareService) router.getService(ShareService.class.getSimpleName());
fragment = service.getShareFragment();
ft = getSupportFragmentManager().beginTransaction();
ft.add(R.id.tab_content, fragment).commitAllowingStateLoss();
}

由于组件是动态加载和卸载的,在使用ShareService的需要进行判空处理。我们看到数据的传输是通过一个中央路由Router来实现的,这个Router就是一个HashMap

UI跳转

页面(activity)的跳转也是通过一个中央路由UIRouter来实现,这里增加了一个优先级的概念。
页面的跳转通过短链的方式,跳转到share的activity

1
UIRouter.getInstance().openUri(getActivity(), "componentdemo://share", null);

share组件在自己实现的ShareUIRouter中声明了自己处理这个短链(scheme和host)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static final String SCHEME = "componentdemo";
private static final String SHAREHOST = "share";
public boolean openUri(Context context, Uri uri, Bundle bundle) {
if (uri == null || context == null) {
return true;
}
String host = uri.getHost();
if (SHAREHOST.equals(host)) {
Intent intent = new Intent(context, ShareActivity.class);
intent.putExtras(bundle == null ? new Bundle() : bundle);
context.startActivity(intent);
return true;
}
return false;
}

如果已经组件已经响应了这个短链,就返回true,这样更低优先级的组件就不会接收到这个短链。注解生成根据scheme和host跳转的逻辑,如ARouter的开源框架已实现。

集成调试

由app或者其他组件充当host的角色,引入其他相关的组件一起参与编译,从而测试整个交互流程。app和组件都可以充当host的角色。在这里我们以app为例。

  1. 在根项目的gradle.properties中增加一个变量mainmodulename,其值就是工程中的主项目,这里是app。设置为mainmodulename的module,isRunAlone永远是true。
  2. 在app项目的gradle.properties文件中增加两个变量:
    1
    2
    debugComponent=com.mrzhang.share:sharecomponent
    compileComponent=sharecomponent
    其中debugComponent是debug的时候引入的组件,compileComponent是release下引入的组件。
    debugComponent引入的组件写法是不同的,组件引入支持两种语法,module直接引用module工程,modulePackage:module,使用componentrelease中已经发布的aar。
    在集成调试中,要引入的share组件是不需要把自己的isRunAlone修改为false的。一个application工程是不能直接引用(compile)另一个application工程的,所以app和组件都是isRunAlone=true在正常情况下是编译不过的。
    插件会自动识别当前要调试的具体是哪个组件,然后把其他组件修改为library工程,而且这个修改只在当次编译生效。

通过task来判断判断当前要运行的是app还是哪个组件,判断的规则如下:
assembleRelease → app
app:assembleRelease或者 :app:assembleRelease → app
sharecomponent:assembleRelease 或者:sharecomponent:assembleRelease→ sharecomponent
这样每个组件可以直接在Androidstudio中run,也可以使用命令进行打包,不需要修改任何配置,却可以自动引入依赖的组件,在开发中可以提高工作效率。

代码边界

依赖的组件集成到host中,本质还是使用compile project(…)或者compile modulePackage:module@aar。不直接在build.gradle中直接引入是为了组件之间的完全隔离,可以称之为代码边界。为了避免直接引入实现类来编程,绕过了面向接口编程的约束。
从task入手,只有在assemble任务的时候才进行compile引入。这样在代码的开发期间,组件是完全不可见的。具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 自动添加依赖,只在运行assemble任务的才会添加依赖,在开发期间组件之间是完全无感知,做到完全隔离的关键
* module直接引用module工程,modulePackage:module,使用componentrelease中已经发布的aar
* @param assembleTask
* @param project
*/
private void compileComponents(AssembleTask assembleTask, Project project) {
String components;
if (assembleTask.isDebug) {
components = (String) project.properties.get("debugComponent")
} else {
components = (String) project.properties.get("compileComponent")
}
if (components == null || components.length() == 0) {
return;
}
String[] compileComponents = components.split(",")
if (compileComponents == null || compileComponents.length == 0) {
return;
}
for (String str : compileComponents) {
if (str.contains(":")) {
File file = project.file("../componentrelease/" + str.split(":")[1] + "-release.aar")
if (file.exists()) {
project.dependencies.add("compile", str + "-release@aar")
} else {
throw new RuntimeException(str + " not found ! maybe you should generate a new one ")
}
} else {
project.dependencies.add("compile", project.project(':' + str))
}
}
}

生命周期

集成调试,可以在打包的时候把依赖的组件参与编译,此时反编译apk的代码会看到各个组件的代码和资源都已经包含在包里面。但是由于每个组件的唯一入口ApplicationLike还没有执行oncreate()方法,所以组件并没有把自己的服务注册到中央路由,因此组件实际上是不可达的。

加载组件目前插件提供了两种方式,字节码插入和反射调用。

字节码插入模式

在dex生成之前,扫描所有的ApplicationLike类,它有一个共同的父类,然后通过javassist在主项目的Application.onCreate()中插入调用ApplicationLike.onCreate()的代码。这样就相当于每个组件在application启动的时候就加载起来了。

反射调用的方式

手动在Application.onCreate()中或者在其他合适的时机手动通过反射的方式来调用ApplicationLike.onCreate()。比起字节码插入,这种方式有两个好处,一是字节码插入对代码进行扫描和插入会增加编译的时间,在debug的时候会影响效率,并且这种模式对Instant Run支持不好;二是可以更灵活的控制加载或者卸载时机。
这两种模式的配置是通过配置插件的Extension来实现的,下面是字节码插入的模式下的配置格式,添加applicatonName的目的是加快定位Application的速度。

1
2
3
4
combuild {
applicatonName = 'com.mrzhang.component.application.AppApplication'
isRegisterCompoAuto = true
}

组件化集成步骤

以官方的demo为例子:

  1. 将项目中的所有第三方库引用添加到basiclib的build.gradle
    1
    2
    compile 'com.squareup.okhttp3:okhttp:3.4.1'
    compile 'com.squareup.picasso:picasso:2.5.2'
  2. 将项目中的公共资源如颜色,字符串,风格 ,主要是values里面的东西迁移到basicres
  3. 导入gradle插件
    ComExtension 是否自动注册管理
    ComBuild gradle task 对组件进行打包
    ComCodeTransform ConvertUtils 工具类
  4. componentlib 组件化的基础库 Router/UIRouter的实现
  5. componentservice 定义组件提供的服务接口声明,这里是阅读组件提供了一个fragment
    1
    2
    3
    public interface ReadBookService {
    Fragment getReadBookFragment();
    }
  6. readerComponent
    ui路由的注册,实现IApplicationLike接口,此处使用的是默认的ui路由
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class ReaderAppLike implements IApplicationLike {

    Router router = Router.getInstance();

    @Override
    public void onCreate() {
    router.addService(ReadBookService.class.getSimpleName(), new ReadBookServiceImpl());
    }

    @Override
    public void onStop() {
    router.removeService(ReadBookService.class.getSimpleName());
    }
    }

实现提供的ReadBookService接口

1
2
3
4
5
6
public class ReadBookServiceImpl implements ReadBookService {
@Override
public Fragment getReadBookFragment() {
return new ReaderFragment();
}
}

需要提供的fragment实现,点击后跳转到share的activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ReaderFragment extends Fragment {

private View rootView;

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
if (rootView == null) {
rootView = inflater.inflate(R.layout.readerbook_fragment_reader, container,
false);
rootView.findViewById(R.id.tv_content).setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
UIRouter.getInstance().openUri(getActivity(), "componentdemo://share", null);
}
});
}
return rootView;
}
}
  1. shareComponent
    ui路由的注册,实现IApplicationLike接口,使用的是自定义的ui路由
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class ShareApplike implements IApplicationLike {

    UIRouter uiRouter = UIRouter.getInstance();
    ShareUIRouter shareUIRouter = ShareUIRouter.getInstance();

    @Override
    public void onCreate() {
    uiRouter.registerUI(shareUIRouter);
    }

    @Override
    public void onStop() {
    uiRouter.unregisterUI(shareUIRouter);
    }
    }
    自定义ui路由,实现IComponentRouter接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    public class ShareUIRouter implements IComponentRouter {

    private static final String SCHME = "componentdemo";

    private static final String SHAREHOST = "share";

    private static String[] HOSTS = new String[]{SHAREHOST};

    private static ShareUIRouter instance = new ShareUIRouter();

    private ShareUIRouter() {

    }

    public static ShareUIRouter getInstance() {
    return instance;
    }

    @Override
    public boolean openUri(Context context, String url, Bundle bundle) {
    if (TextUtils.isEmpty(url) || context == null) {
    return true;
    }
    return openUri(context, Uri.parse(url), bundle);
    }

    @Override
    public boolean openUri(Context context, Uri uri, Bundle bundle) {
    if (uri == null || context == null) {
    return true;
    }
    String host = uri.getHost();
    if (SHAREHOST.equals(host)) {
    Intent intent = new Intent(context, ShareActivity.class);
    intent.putExtras(bundle == null ? new Bundle() : bundle);
    context.startActivity(intent);
    return true;
    }
    return false;
    }

    @Override
    public boolean verifyUri(Uri uri) {
    String scheme = uri.getScheme();
    String host = uri.getHost();
    if (SCHME.equals(scheme)) {
    for (String str : HOSTS) {
    if (str.equals(host)) {
    return true;
    }
    }
    }
    return false;
    }
    }

activity的具体实现

1
2
3
4
5
6
7
8
9
public class ShareActivity extends AppCompatActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.share_activity_share);
}

}

最后在主app中调用:
使用shareComponent提供的fragment

1
2
3
4
5
6
7
Router router = Router.getInstance();
if (router.getService(ReadBookService.class.getSimpleName()) != null) {
ReadBookService service = (ReadBookService) router.getService(ReadBookService.class.getSimpleName());
fragment = service.getReadBookFragment();
ft = getSupportFragmentManager().beginTransaction();
ft.add(R.id.tab_content, fragment).commitAllowingStateLoss();
}

注册activity

1
Router.registerComponent("com.mrzhang.share.applike.ShareApplike");

Router实现解析

不管是哪个组件化方案,路由技术都是必不可少的,得到团队的这个方案路由未采用第三方,且实现了动态加载和卸载,确实不错。

  1. 定义组件ui路由接口,组件自定义ui路由需实现这个接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public interface IComponentRouter {

    /**
    * 打开一个链接
    *
    * @param url
    * 目标url可以是http 或者 自定义scheme
    * @param bundle
    * 打开目录activity时要传入的参数。建议只传基本类型参数。
    * @return 是否正常打开
    */
    public boolean openUri(Context context, String url, Bundle bundle);

    public boolean openUri(Context context,Uri uri, Bundle bundle);

    public boolean verifyUri(Uri uri);
    }
  2. 实现一个统一管理ui路由的接口,继承IComponentRouter
    设置优先级和提供注册方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public interface IUIRouter extends IComponentRouter {

    int PRIORITY_NORMAL = 0;
    int PRIORITY_LOW = -1000;
    int PRIORITY_HEIGHT = 1000;

    void registerUI(IComponentRouter router, int priority);

    void registerUI(IComponentRouter router);

    void unregisterUI(IComponentRouter router);
    }
  3. 实现一个统一管理的ui路由
    定义了一个路由列表和优先级map
    1
    2
    List<IComponentRouter> uiRouters = new ArrayList<IComponentRouter>();
    HashMap<IComponentRouter, Integer> priorities = new HashMap<IComponentRouter, Integer>();
    提供单例对象的接口:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private UIRouter() {
    }
    public static UIRouter getInstance() {
    if (instance == null) {
    synchronized (UIRouter.class) {
    if (instance == null) {
    instance = new UIRouter();
    }
    }
    }
    return instance;
    }
    注册一个ui路由并设置优先级,如果当前列表存在这个路由且优先级相同则返回,移除已存在的路由对象,遍历路由列表,当优先级小于当前路由时,退出遍历并插入列表,同时插入优先级到优先级map
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Override
    public void registerUI(IComponentRouter router, int priority) {
    if (priorities.containsKey(router) && priority == priorities.get(router)) {
    return;
    }
    removeOldUIRouter(router);
    int i = 0;
    for (IComponentRouter temp : uiRouters) {
    Integer tp = priorities.get(temp);
    if (tp == null || tp <= priority) {
    break;
    }
    i++;
    }
    uiRouters.add(i, router);
    priorities.put(router, Integer.valueOf(priority));
    }
    反注册执行移除操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Override
    public void unregisterUI(IComponentRouter router) {
    for (int i = 0; i < uiRouters.size(); i++) {
    if (router == uiRouters.get(i)) {
    uiRouters.remove(i);
    priorities.remove(router);
    break;
    }
    }
    }
    统一对ui理由的url进行处理,没有特殊前缀加上http头返回
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Override
    public boolean openUri(Context context, String url, Bundle bundle) {
    url = url.trim();
    if (!TextUtils.isEmpty(url)) {
    if (!url.contains("://") &&
    (!url.startsWith("tel:") ||
    !url.startsWith("smsto:") ||
    !url.startsWith("file:"))) {
    url = "http://" + url;
    }
    Uri uri = Uri.parse(url);
    return openUri(context, uri, bundle);
    }
    return true;
    }
    遍历路由列表,调用当前ui路由的检查方法和跳转方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Override
    public boolean openUri(Context context, Uri uri, Bundle bundle) {
    for (IComponentRouter temp : uiRouters) {
    try {
    if (temp.verifyUri(uri) && temp.openUri(context, uri, bundle)) {
    return true;
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    return false;
    }
  4. 路由实现类Router,负责组件的服务管理和组件注册,同时调用applicationLike的onCreate和onStop,相当于Application的初始化与销毁的生命周期
    组件的服务map和注册组件map
    1
    2
    3
    private HashMap<String, Object> services = new HashMap<>();
    //注册组件的集合
    private static HashMap<String, IApplicationLike> components = new HashMap<>();
    提供单例对象的接口:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    private Router() {
    }

    public static Router getInstance() {
    if (instance == null) {
    synchronized (Router.class) {
    if (instance == null) {
    instance = new Router();
    }
    }
    }
    return instance;
    }
    组件服务的添加移除和获取
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public synchronized void addService(String serviceName, Object serviceImpl) {
    if (serviceName == null || serviceImpl == null) {
    return;
    }
    services.put(serviceName, serviceImpl);
    }

    public synchronized Object getService(String serviceName) {
    if (serviceName == null) {
    return null;
    }
    return services.get(serviceName);
    }

    public synchronized void removeService(String serviceName) {
    if (serviceName == null) {
    return;
    }
    services.remove(serviceName);
    }
    组件的注册与反注册,调用此处可实现动态的加载和卸载组件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    /**
    * 注册组件
    *
    * @param classname 组件名
    */
    public static void registerComponent(@Nullable String classname) {
    if (TextUtils.isEmpty(classname)) {
    return;
    }
    if (components.keySet().contains(classname)) {
    return;
    }
    try {
    Class clazz = Class.forName(classname);
    IApplicationLike applicationLike = (IApplicationLike) clazz.newInstance();
    applicationLike.onCreate();
    components.put(classname, applicationLike);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    /**
    * 反注册组件
    *
    * @param classname 组件名
    */
    public static void unregisterComponent(@Nullable String classname) {
    if (TextUtils.isEmpty(classname)) {
    return;
    }
    if (components.keySet().contains(classname)) {
    components.get(classname).onStop();
    components.remove(classname);
    return;
    }
    try {
    Class clazz = Class.forName(classname);
    IApplicationLike applicationLike = (IApplicationLike) clazz.newInstance();
    applicationLike.onStop();
    components.remove(classname);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }