Android组件化方案实践

前言

从之前就一直听说对应用进行组件化,很是火热,也出现了很多组件化方案的开源库。于是查阅了很多的学习资料,看了几个开源的组件化实战例子与组件化方案的开源库,总结后尝试去对之前做过的一个项目进行组件化,也是对学习到的进行实践与检验。

项目地址:github.com/asendiLin/B…,欢迎start

正文

什么是组件化?

组件:可以想象成一辆车,车由车轮、发动机、方向盘等组成,这些物件可以认为是组件。

组件化是指解耦复杂系统时将多个功能模块拆分、重组的过程,有多种属性、状态反映其内部特性。(个人理解,如有不妥欢迎指正)

为何要组件化?

先来看看组件化之前的应用结构:

组件化之前

存在的问题:

  • 将功能分包,随着功能的迭代,项目会越来越庞大,很容易失去层次感
  • 编译一次的时间很久
  • 在多人合作开发的情况下,很容易出现冲突的情况
  • 新加入的成员需要花大量时间去熟悉代码
  • 由于没有分模块,业务与业务之间很可能在无意间加入耦合(显式调用一时爽,一直调用一直爽)

然后看一下组件化之后的结构:

组件化之后

解决的问题:

  • 分成不同的组件,各个层次的组件结构清晰
  • 组件单独编译,时间减少了很多
  • 多人开发情况下,有更多的精力去注意负责的组件上,并且减低代码冲突的可能性
  • 组件化开发后,各模块大大降低了依赖程度(通过接口向外暴露提供的服务),即使依赖也不会显式依赖。

组件化方案

  • 路由:本质是类的查找。先把类放到一个容器中,需要用到的时候,根据所提供的字符串查找得到存放在容器中的类。需要打包在同一个app才能够通信。

  • 组件总线:本质是转发与调用。组件总线手机所以组建类并形成映射表。调用时总线根据字符串找到对应的组建类并将调用消息转发给该组件类,组件执行完后通过组件总线将结果返回给调用方。组件总线只负责通信,不需要接口下沉,可跨进程通信,所以可调用其他app,所有的组件是平行的,没有依赖关系。

开元组件化方案对比

  • CC

    基于组件总线、支持渐进式改造的、支持跨进程调用的Android组件化框架。

    • 接入后可立即用组件的方式开发新业务,无需修改项目中现有的代码,只需新增组件类便可支持新组建的调用。
    • 支持在应用内跨进程调用组件,跨app调用组件。调用方式与同一进程内调用方式一致。
    • 将组件之间的关系拍平,在无互相依赖的情况下能够互相调用。
  • ARouter

    一个用于帮助 Android App 进行组件化改造的框架,支持模块间的路由、通信、解耦。

    • 跨模块页面跳转,可以传参
    • 拦截跳转过程
    • 跨模块api调用
    • 依赖注入
  • WMRouter

    一款Android路由框架,基于组件化的设计思路,功能灵活,使用简单。主要提供URI分发、ServiceLoader两大功能。

个人组件化实践

拆分维度: 按功能拆分、在拆分过程中,如果会因为某个模块而依赖主模块,则将该模块拆分出来,比如用户模块,基本会从头到尾贯穿整个项目。

  1. 划分组件:
  • app壳层:该层没有任何的业务逻辑,只是app的启动页。

  • 业务组件层

    订单组件、社区组件、个人组件、课程表组件、登录组件、主界面组件

  • 功能组件层

    公用第三方依赖、数据库

组件化后的结构 2. 在工程的目录下创建一个config.gradle文件,用于统一管理版本控制。例如:

//统一版本控制
ext{

    isApplication = false

    android = [
            "minSdkVersion" : 16,
            "targetSdkVersion" : 26,
            "compileSdkVersion" : 28
    ]

    dependencies = [
            "appcompat_v7" : 'com.android.support:appcompat-v7:28.0.0',
            "constraint_layout" :  'com.android.support.constraint:constraint-layout:1.1.3',
            "junit" : 'junit:junit:4.12',

            "lifecycle" : 'android.arch.lifecycle:extensions:1.1.1',

            "dagger_api" : 'com.google.dagger:dagger:2.16',
            "dagger_compiler" : 'com.google.dagger:dagger-compiler:2.16',
            "dagger_processor" : 'com.google.dagger:dagger-android-processor:2.16',
            "dagger_android" : 'com.google.dagger:dagger-android:2.16',
            "dagger_android_support" : 'com.google.dagger:dagger-android-support:2.16',

            "arouter_api" : 'com.alibaba:arouter-api:1.5.0',
            "arouter_compiler" : 'com.alibaba:arouter-compiler:1.2.2',

            "eventbus_api" : 'org.greenrobot:eventbus:3.1.1',
            "eventbus_annotation_processor" : 'org.greenrobot:eventbus-annotation-processor:3.1.1',

            "kotlinx_coroutines_core" : 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.20',
            "kotlinx_coroutines_android" : 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.20',

            "glide" : 'com.github.bumptech.glide:glide:3.7.0'
    ]
}
  1. 集成模式与组件模式的转换

在main文件夹下创建debug文件夹,用于存放开发阶段的文件,比如AndroidManifest.xml,然后在每个组件的build.gradle文件中修改为:

 if (rootProject.ext.isApplication) {
     apply plugin: 'com.android.application'
 } else {
     apply plugin: 'com.android.library'
 }
 ......
 
 
 android {
     compileSdkVersion 28
 
     defaultConfig {
         ......
         if (rootProject.ext.isApplication) {
             applicationId "com.sendi.community"
         }
 
 //        resourcePrefix "order_"
         ......
     }
 
     ......
     }
 
     sourceSets {
         main {
             java.srcDirs = ['src/main/java', 'src/main/java/']
             if (rootProject.ext.isApplication) {//开发时使用的清单文件
                 manifest.srcFile 'src/main/debug/AndroidManifest.xml'
             } else {//集成时使用的清单文件
                 manifest.srcFile 'src/main/AndroidManifest.xml'
             }
         }
     }
 
     lintOptions {
         abortOnError false
     }
 }
 
 dependencies {
     ......
 }
  1. 每个组件创建一份对应组件模式(debug文件下)的组件清单(AndroidManifest.xml)

  2. 公共资源的抽取、公共依赖的抽取

    将公共的资源抽取到公共的module中,比如公用的图片、style、color等

  3. 组件间的调用与通信:

    模块A的某个Activity在AModule、所以BModule无法引用该Activity。 组件化后,各个组件是无法显式调用的,所以需要有类似路由或事件总线的机制进 行寻找目标进行调用。

    使用事件发送: 发送对应的事件,然后做出对应的操作。存在问题是可能会定 义很多的事件,很难找出发送事件端与接收事件端,发送的事件没有约束,可能会出现重名情况。

    采用了路由+暴露接口供外界调用+消息总线: 阿里开源的ARouter路由框架进行。使用简单,只需定义路由路径,参数传递方便。(接口管理成本高)

ARouter的基本使用

  1. 添加依赖和配置:
 apply plugin: 'kotlin-kapt'
 android{
     defaultConfig{
         kapt {
             arguments {
                 arg("AROUTER_MODULE_NAME", project.getName())
             }
         }
     }
 }
 dependencies { 
     implementation 'com.alibaba:arouter-api:1.5.0'
     kapt 'com.alibaba:arouter-compiler:1.2.2'
 }
  1. 初始化:
 ARouter.openLog()
 ARouter.openDebug()
 ARouter.printStackTrace()
 ARouter.init(this)
  1. 跨组件调用Activity:
//在activity上添加注解
@Route(path = ONE_ACTIVITY)
class OneActivity : AppCompatActivity() ,View.OnClickListener {}
//跳转到OneActivity
ARouter.getInstance().build(ONE_ACTIVITY).navigation(this)
  1. 跨组件获取Fragment:
@Route(path = ONE_FRAGMENT)
class AFragment : Fragment() {}

//获取Fragment
val fragment =  ARouter.getInstance().build(ONE_FRAGMENT).navigation() as Fragment
  1. 跨组件调用提供的接口服务:
//定义接口,需要继承IProvider
interface IManager : IProvider{
    fun doManage()
}

//实现要对外提供的接口,并添加注解
@Route(path = "/my/manager")
class Manager : IManager {

    private val myTag = "Manager"

    override fun doManage() {
        Log.i(myTag,"doManage")
    }

    override fun init(context: Context?) {
        Log.i(myTag,"context is null == ${context==null}")
    }
}

//外部调用接口的实现,可通过依赖注入,也可通过路由
val iManager = ARouter.getInstance().navigation(IManager::class.java)

@Autowired(name = "/my/manager")
lateinit var iManager : IManager
  1. 拦截器:拦截器用于在跳转的中间插入要拦截的逻辑,通过注解@Interceptor进行注册,在该注解上可指定拦截器的优先级,值越小,优先级越大。
@Interceptor(priority = 1,name = "testInterceptor")
class TestInterceptor : IInterceptor {

    private val myTag = "TestInterceptor"

    override fun init(context: Context?) {
        Log.i(myTag,"init")
    }

    override fun process(postcard: Postcard?, callback: InterceptorCallback?) {
        Log.i(myTag,"process path=${postcard?.path}")
        //拦截操作
        callback?.onContinue(postcard)
    }
}

本实践中组件间调用与通信: 调用与通信,如果没有具体的依赖,则无法显示调用,因为在编译时已经隔离了,那么需要采用路由的方式来隐式调用。

  • 组件间activity跳转:
ARouter.getInstance().build("LOGIN_PATH").navigation(this)
  • 跨组件调用: 如果组件需要向外提供服务无,则创建一个用于向外暴露服务接口的module,采用接口的方式进行暴露,然后对应的module进行对应的实现,然后在实现类使用注解。

    如果别的组件需要使用到该组件的服务,则依赖向外暴露服务接口的module,然后使用依赖注入或者路由的方式获取具体的实现,最后达到组件间调用服务的目的。

组件间的通信

于是,到了最后整个工程的结构为:

工程最后的组件化结果

组件的集成

集成的时候,只需到config.gradle文件中将 isApplication 设置为false,然后将所有的业务组件与用到的基础组件引入到app组件中:

dependencies {
    //......
    implementation project(':core')
    implementation project(':base')
    implementation project(':order')
    implementation project(':community')
    implementation project(':myself')
    implementation project(':login')
    implementation project(':user')
    implementation project(':course')
    implementation project(':home')
	//......
}

踩过的坑:

  • 在移动各个组件的时候,最开始没有先提前移好基础组件
  • 集成的时候各个组件的初始化时机把握好

注意的事情:

  • 资源重名:提前限制资源命名->resourcePrefix "custom_", 资源文件名需要加入custom_,否则会提示,但可以编译通过。
  • 接口的定义的粒度要精准,否则会出现很多接口,导致难以管理。

路由原理

ARouter的结构:

  • arouter-annotation:注解的声明和信息存储类的模块
  • arouter-compiler:编译期解析注解信息并生成相应类以便进行注入的模块
  • arouter-api:核心调用Api功能的模块

收集与存放

编译时生成的类:由RouteProcessor生成

  • ARouter Group$$path:将对应组的注解信息进行收集,映射关系:路径——RouteMeta
public class ARouter$$Group$$path implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/path/a_fragment", RouteMeta.build(RouteType.FRAGMENT, AFragment.class, "/path/a_fragment", "path", null, -1, -2147483648));
    atlas.put("/path/b_activity", RouteMeta.build(RouteType.ACTIVITY, BActivity.class, "/path/b_activity", "path", null, -1, -2147483648));
    atlas.put("/path/service", RouteMeta.build(RouteType.PROVIDER, ServiceImpl.class, "/path/service", "path", null, -1, -2147483648));
  }
}
  • ARouter Providers$$app:用于收集实现了IProvider的集合,索引到以类全限定名作为key

对应Warehouse.providersIndex映射

public class ARouter$$Providers$$app implements IProviderGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> providers) {
    providers.put("com.example.arouterdemo.service.IService", RouteMeta.build(RouteType.PROVIDER, ServiceImpl.class, "/path/service", "path", null, -1, -2147483648));
  }
}
  • ARouter Root$$app:收集所有group的信息,以组名为key,生成对应组的类的class对象为value

对应Warehouse.groupsIndex映射

public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("path", ARouter$$Group$$path.class);
  }
}

ARouter的初始化:采用了外观模式,实际操作是由_ARouter执行的。

ARouter.init

   public static void init(Application application) {
        if (!hasInit) {
            logger = _ARouter.logger;
            _ARouter.logger.info(Consts.TAG, "ARouter init start.");
            hasInit = _ARouter.init(application);

            if (hasInit) {
                _ARouter.afterInit();
            }

            _ARouter.logger.info(Consts.TAG, "ARouter init over.");
        }
    }

Arouter采用的外观者模式,实际的初始化等操作交由_ARouter。 _ARouter.init

protected static synchronized boolean init(Application application) {
        mContext = application;
        LogisticsCenter.init(mContext, executor);
        logger.info(Consts.TAG, "ARouter init success!");
        hasInit = true;
        mHandler = new Handler(Looper.getMainLooper());

        return true;
    }

LogisticsCenter.init


public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
    			 //......
        
               routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
                 //遍历com.alibaba.android.arouter.routes包下所有的类名,
                 //找到生成类对应的类名,然后调用loadInto方法进行存放注解生成的信息,然后存放在Warehouse中的静态映射类中
                for (String className : routerMap) {
                    if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                        // This one of root elements, load root.
                        ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                    } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                        // Load interceptorMeta
                        ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                    } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                        // Load providerIndex
                        ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                    }
                }
            }

        //......
    }

  1. 找到由注解生成的类的类名
  2. 找到以上对应类名,然后调用loadInto方法进行存放注解生成的信息,然后存放在Warehouse中的静态映射类中

调用与跳转

_ARouter. navigation方法:

protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
   //......

    try {
        LogisticsCenter.completion(postcard);//1
    } catch (NoRouteFoundException ex) {
        logger.warning(Consts.TAG, ex.getMessage());

        //......

        if (null != callback) {
            callback.onLost(postcard);//2
        } else {
            //......
        }

        return null;
    }

    if (null != callback) {
        callback.onFound(postcard);//3
    }

    //4.非绿色通道
    if (!postcard.isGreenChannel()) {  
        interceptorService.doInterceptions(postcard, new InterceptorCallback() {
         //......
        });
    } else {
        //直接走到跳转
        return _navigation(context, postcard, requestCode, callback);
    }
    return null;
}
  1. 补充postcard的信息。Postcard继承于RouteMeta,里边存放的是路由所需的信息,path、group、传递的参数等
  2. 路由出错,回调onLost(postcard)
  3. 路由成功,回调callback.onFound(postcard)
  4. 如果为非绿色通道,则会执行拦截器,否则直接进入跳转

LogisticsCenter.completion(postcard):

public synchronized static void completion(Postcard postcard) {
    //判空操作

    RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
    if (null == routeMeta) {   
        Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());
        if (null == groupMeta) {
            //抛出没有找到的异常
        } else {
            //......
           IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
           iGroupInstance.loadInto(Warehouse.routes);
           Warehouse.groupsIndex.remove(postcard.getGroup());

            completion(postcard);   // 再加载一遍
        }
    } else {
       //补充postCard的信息
    }
}

在执行这个函数之前,postcard只有path和group信息

  1. 从Warehouse.routes获取路由元数据(通过path获取RouteMeta)

    a. 获取不到,通过Warehouse.groupsIndex根据group索引而得到对应的组别IRouteGroup(ARouter Group path)的class b. 反射获取到组别IRouteGroup的(ARouter Group$$path)实例 c. 调用loadInto方法,将信息添加到Warehouse.routes中去 d. Warehouse.groupsIndex移除索引为group的元数据,防止重加载 e. 然后重新调用方法completion本身

  2. 将获取到postcard进行信息的填充

_navigation方法:根据类型进行解析与跳转

  private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        final Context currentContext = null == context ? mContext : context;

        switch (postcard.getType()) {
            case ACTIVITY://活动的跳转
                // Build intent
                final Intent intent = new Intent(currentContext, postcard.getDestination());
                intent.putExtras(postcard.getExtras());

                // Set flags.
                int flags = postcard.getFlags();
                if (-1 != flags) {
                    intent.setFlags(flags);
                } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                // Set Actions
                String action = postcard.getAction();
                if (!TextUtils.isEmpty(action)) {
                    intent.setAction(action);
                }

                // Navigation in main looper.
                runInMainThread(new Runnable() {
                    @Override
                    public void run() {
                        startActivity(requestCode, currentContext, intent, postcard, callback);
                    }
                });

                break;
            case PROVIDER://接口实现类实例
                return postcard.getProvider();
            case BOARDCAST:
            case CONTENT_PROVIDER:
            case FRAGMENT://Fragment实例获取
                Class fragmentMeta = postcard.getDestination();
                try {
                    Object instance = fragmentMeta.getConstructor().newInstance();
                    if (instance instanceof Fragment) {
                        ((Fragment) instance).setArguments(postcard.getExtras());
                    } else if (instance instanceof android.support.v4.app.Fragment) {
                        ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
                    }

                    return instance;
                } catch (Exception ex) {
                    logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
                }
            case METHOD:
            case SERVICE:
            default:
                return null;
        }

        return null;
    }

结语

这次的研究与实践大约花了一个多月的时间,学到的不止是组件化的方案,而且还学会了它的思想,尽量让不同业务解耦,需要通信的时候采用路由与接口的方式进行提供,这样便符合了设计的原则。组件化带来的好处有很多,但是对于小项目进行组件化,可能受益并不理想,甚至小题大做,所以需要中和好,选出最合适的方案。

猜你喜欢

转载自juejin.im/post/7124120644521820190