关于Android模块化我有一些话不知当讲不当讲

最近公司一个项目使用了模块化设计,本人参与其中的一个小模块开发,但是整体的设计并不是我架构设计的,开发半年有余,在此记录下来我的想法。

关于Android模块化我有一些话不知当讲不当讲
模块化场景
架构演变
技术要点
Library module
Library module开发问题
依赖管理
数据通信
页面路由跳转
Interface和Implement
EventBus
映射匹配
Map register
APT
Gradle Transform
映射匹配总结
开发调试技巧
Debug
容器设计
CI
总结
相关资料
模块化场景

为什么需要模块化?

当一个App用户量增多,业务量增长以后,就会有很多开发工程师参与同一个项目,人员增加了,原先小团队的开发方式已经不合适了。

原先的一份代码,现在需要多个人来维护,每个人的代码质量也不相同,在进行代码Review的时候,也是比较困难的,同时也容易会产生代码冲突的问题。

同时随着业务的增多,代码变的越来越复杂,每个模块之间的代码耦合变得越来越严重,解耦问题急需解决,同时编译时间也会越来越长。

人员增多,每个业务的组件各自实现一套,导致同一个App的UI风格不一样,技术实现也不一样,团队技术无法得到沉淀。

架构演变

在刚刚开始的时候,项目架构使用的是MVP模式,这也是最近几年很流行的一个架构方式,下面是项目的原始设计。

MVP

随着业务的增多,我们添加了Domain的概念,Domain从Data中获取数据,Data可能会是Net,File,Cache各种IO等,然后项目架构变成了这样。

MVP2

再然后随着人员增多,各种基础组件也变的越来越多,业务也很复杂,业务与业务之间还有很强的耦合,就变成了这样的。

使用模块化技术以后,架构变成了这样。

组件化架构

技术要点

这里简单介绍下Android项目实现模块化需要使用的技术以及技术难点。

Library module

在开始开始进行模块化之前,需要把各个业务单独抽取成Android Library Module,这个是Android Studio自带一个功能,可以把依赖较少的,作为基本组件的抽取成一个单独模块。

如图所示,我把各个模块单独分为一个独立的项目。

组件化架构

在主项目中使用gradle添加代码依赖。

// common
compile project(':ModuleBase')
compile project(':ModuleComponent')
compile project(':ModuleService')

// biz
compile project(':ModuleUser')
compile project(':ModuleOrder')
compile project(':ModuleShopping')
1
2
3
4
5
6
7
8
9
Library module开发问题

在把代码抽取到各个单独的Library Module中,会遇到各种问题。最常见的就是R文件问题,Android开发中,各个资源文件都是放在res目录中,在编译过程中,会生成R.java文件。R文件中包含有各个资源文件对应的id,这个id是静态常量,但是在Library Module中,这个id不是静态常量,那么在开发时候就要避开这样的问题。

举个常见的例子,同一个方法处理多个view的点击事件,有时候会使用switch(view.getId())这样的方式,然后用case R.id.btnLogin这样进行判断,这时候就会出现问题,因为id不是经常常量,那么这种方式就用不了。

同样开发时候,用的最多的一个第三方库就是ButterKnife,ButterKnife也是不可以用的,在使用ButterKnife的时候,需要用到注解配置一个id来找到对应view,或者绑定对应的各种事件处理,但是注解中的各个字段的赋值也是需要静态常量,那么就不能够使用ButterKnife了。

解决方案有下面几种:

1.重新一个Gradle插件,生成一个R2.java文件,这个文件中各个id都是静态常量,这样就可以正常使用了。

2.使用Android系统提供的最原始的方式,直接用findViewById以及setOnClickListener方式。

3.设置项目支持Databinding,然后使用Binding中的对象,但是会增加不少方法数,同时Databinding也会有编译问题和学习成本,但是这些也是小问题,个人觉的问题不大。

上面是主流的解决方法,个人推荐的使用优先级为 3 > 2 > 1。

当把个模块分开以后,每个人就可以单独分组对应的模块就行了,不过会有资源冲突问题,个人建议是对各个模块的资源名字添加前缀,比如user模块中的登录界面布局为activity_login.xml,那么可以写成这样us_activity_login.xml。这样就可以避免资源冲突问题。同时Gradle也提供的一个字段resourcePrefix,确保各个资源名字正确,具体用法可以参考官方文档。

依赖管理

当完成了Library module后,代码基本上已经很清晰了,跟我们上面的最终架构已经很相似了,有了最基本的骨架,但是还是没有完成,因为还是多个人操作同一个git仓库,各个开发小伙伴还是需要对同一个仓库进行各种fork和pr。

随着对代码的分割,但是主项目app的依赖变多了,如果修改了lib中的代码,那么编译时间是很恐怖的,大概统计了一下,原先在同一个模块的时候,编译时间大概需要2-3min,但是分开以后大概需要5-6min,这个是绝对无法忍受的。

上面的第一问题,可以这样解决,把各个子module分别使用单独的一个git仓库,这样每个人也只需要关注自己需要的git仓库即可,主仓库使用git submodule的方式,分别依赖各个子模块。

但是这样还是无法解决编译时间过长的问题,我们把各个模块也单独打包,每次子模块开发完成以后,发布到maven仓库中,然后在主项目中使用版本进行依赖。

举个例子,比如进行某一版本迭代,这个版本叫1.0.0,那么各个模块的版本也叫同样的版本,当版本完成测试发布后,对各个模块打对应版本的tag,然后就很清楚的了解各模块的代码分布。

gradle依赖如下。

// common
compile 'cn.mycommons:base:1.0.0'
compile 'cn.mycommons:component:1.0.0'
compile 'cn.mycommons:service:1.0.0'

// biz
compile 'cn.mycommons:user:1.0.0'
compile 'cn.mycommons:order:1.0.0'
compile 'cn.mycommons:shopping:1.0.0'
1
2
3
4
5
6
7
8
9
可能有人会问,既然各个模块已经分开开发,那么如果进行开发联调,别急,这个问题暂时保留,后面会对这个问题后面再表。

数据通信

当一个大项目拆成若干小项目时候,调用的姿势发生了少许改变。我这边总结了App各个模块之间的数据通信几种方式。

页面跳转,比如在订单页面下单时候,需要判断用户是否登录,如果没有则需要跳到登录界面。
主动获取数据,比如在下单时候,用户已经登录,下单需要传递用户的基本信息。
被动获得数据,比如在切换用户的时候,有时候需要更新数据,如订单页面,需要把原先用户的购物车数据给清空。
再来看下App的架构。

App架构

第一个问题,原先的方式,直接指定某个页面的ActivityClass,然后通过intent跳转即可,但是在新的架构中,由于shopping模块不直接依赖user,那么则不能使用原始的进行跳转,我们解决方式使用Router路由跳转。

第二个问题,原先的方式有个专门的业务单利,比如UserManager,直接可以调用即可,同样由于依赖发生了改变,不能够进行调用。解决方案是所有的需要的操作,定义成接口放在Service中。

第三个问题,原先的方式,可以针对事件变化提供回调接口,当我需要监听某个事件时候,设置回调即可。

页面路由跳转

如上分析,原先方式代码如下。

Intent intent = new Intent(this, UserActivity.class);
startActivity(intent);
1
2
但是使用Router后,调用方式改变了。

RouterHelper.dispatch(getContext(), "app://user");
1
具体的原理是什么,很简单的,做一个简单的映射匹配即可,把"app://user"与UserActivity.class配对,具体的就是定义一个Map,key是对应的Router字符,value是Activity的class。在跳转时候从map中获取对应的ActivityClass,然后在使用原始的方式。

可能有人的会问,要向另外一个页面传递参数怎么办,没事我们可以在router后面直接添加参数,如果是一个复杂的对象那么可以把对象序列化成json字符串,然后再从对应的页面通过反序列化的方式,得到对应的对象。

例如:

RouterHelper.dispatch(getContext(), "app://user?id=123&obj={"name":"admin"}");
1
注: 上面的router中json字符串是需要url编码的,不然会有问题的,这里只是做个示例。

除了使用Router进行跳转外,我想了一下,可以参考Retrofit方式,直接定义跳转Java接口,如果需要传递额外参数,则以函数参数的方式定义。

这个Java接口是没有实现类的,可以使用动态代理方式,然后接下来的方式,和使用Router的方式一样。

那么这总两种方式有什么优缺点呢。

Router方式:

有点:不需要高难度的技术点,使用方便,直接使用字符串定义跳转,可以好的往后兼容
缺点:因为使用的是字符串配置,如果字符输入字符,则很难发现bug,同时也很难知道某个参数对应的含义
仿Retrofit方式:

因为是Java接口定义,所以可以很简单找到对应的跳转方法,参数定义也很明确,可以直接写在接口定义处,方便查阅。
同样因为是Java接口定义,那么如果需要扩展参数,只能重新定义新方法,这样会出现多个方法重载,如果在原先接口上修改,对应的原先调用方也要做响应的修改,比较麻烦。
上面是两种实现方式,如果有相应同学要实现模块化,可以根据实际情况做出选择。

Interface和Implement

如上分析,如果需要从某个业务中获取数据,我们分别需要定义接口以及实现类,然在获取的时候在通过反射来实例化对象。

下面是简单的代码示例

接口定义

public interface IUserService {

String getUserName();
}
1
2
3
4
实现类

class UserServiceImpl implements IUserService {

@Override
public String getUserName() {
return "UserServiceImpl.getUserName";
}
}
1
2
3
4
5
6
7
反射生成对象

public class InjectHelper {

@NonNull
public static AppContext getAppContext() {
return AppContext.getAppContext();
}

@NonNull
public static IModuleConfig getIModuleConfig() {
return getAppContext().getModuleConfig();
}

@Nullable
public static <T> T getInstance(Class<T> tClass) {
IModuleConfig config = getIModuleConfig();
Class<? extends T> implementClass = config.getServiceImplementClass(tClass);
if (implementClass != null) {
try {
return implementClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
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
实际调用

IUserService userService = InjectHelper.getInstance(IUserService.class);
if (userService != null) {
Toast.makeText(getContext(), userService.getUserName(), Toast.LENGTH_SHORT).show();
}
1
2
3
4
本示例中每次调用都是用反射生成新的对象,实际应用中可能与IoC工具结合使用,比如Dagger2.

EventBus

针对上面的第三个问题,原先设计的使用方式也是可以的,只需要把回调接口定义到对应的service接口中,然后调用方就可以使用。

但是我建议可以使用另外一个方式——EventBus,EventBus也是利用观察者模式,对事件进行监听,是设置回调更优雅方式的实现。

优点:不需要定义很多个回调接口,只需要定义事件Class,然后通过Claas的唯一性来进行事件匹配。

缺点:需要定义很多额外的类来表示事件,同时也需要关注EventBus的生命周期,在不需要使用事件时候,需要注销事件绑定,不然容易发生内存泄漏。

映射匹配

上面的介绍的各个模块之间通信,都运涉及到映射匹配问题,在此我总结了一下,主要涉及到一下三种方式。

Map register

Map register是这样的,全局定义一个Map,各个模块在初始化的时候,分别在初始化的时候注册映射关系。

下面是简单的代码示例,比如我们定义一个模块生命周期,用于初始化各个模块。

public interface IModuleLifeCycle {

void onCreate(IModuleConfig config);

void onTerminate();
}
1
2
3
4
5
6
User模块初始化

public class UserModuleLifeCycle extends SimpleModuleLifeCycle {

public UserModuleLifeCycle(@NonNull Application application) {
super(application);
}

@Override
public void onCreate(@NonNull IModuleConfig config) {
config.registerService(IUserService.class, UserServiceImpl.class);
config.registerRouter("app://user", UserActivity.class);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
在Application中完成初始化

public class AppContext extends Application {

private ModuleLifeCycleManager lifeCycleManager;

@Override
public void onCreate() {
super.onCreate();

lifeCycleManager = new ModuleLifeCycleManager(this);
lifeCycleManager.onCreate();
}

@Override
public void onTerminate() {
super.onTerminate();

lifeCycleManager.onTerminate();
}

@NonNull
public IModuleConfig getModuleConfig() {
return lifeCycleManager.getModuleConfig();
}
}

public class ModuleLifeCycleManager {

@NonNull
private ModuleConfig moduleConfig;
@NonNull
private final List<IModuleLifeCycle> moduleLifeCycleList;

ModuleLifeCycleManager(@NonNull Application application) {
moduleConfig = new ModuleConfig();
moduleLifeCycleList = new ArrayList<>();
moduleLifeCycleList.add(new UserModuleLifeCycle(application));
moduleLifeCycleList.add(new OrderModuleLifeCycle(application));
moduleLifeCycleList.add(new ShoppingModuleLifeCycle(application));
}

void onCreate() {
for (IModuleLifeCycle lifeCycle : moduleLifeCycleList) {
lifeCycle.onCreate(moduleConfig);
}
}

void onTerminate(www.xuancayule.com) {
for (IModuleLifeCycle lifeCycle : moduleLifeCycleList) {
lifeCycle.onTerminate(www.yuheng119.com/);
}
}

@NonNull
IModuleConfig getModuleConfig() {
return moduleConfig;

APT

使用注解的方式配置映射信息,然后生成一个类似Database一样的文件,然后Database文件中包含一个Map字段,Map中记录各个映射信息。

首先需要定义个Annotation。

如:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Implements {

Class parent();

需要实现一个 Annotation Process Tool,来解析自己定义的Annotation。

代码略,此代码有点复杂,暂时不贴了。

编译产生的文件,大概如下所示。

public class Implement_$$_Database {

@NonNull
private final Map<Class<?>, Class<?>> serviceConfig;

public Implement_$$_Database(http://www.gouyily.cn/) {

serviceConfig = new HashMap<>();
serviceConfig.put(IUserService.class, UserServiceImpl.class);
}

public <T> Class<? extends T> getServiceImplementClass(Class<T> serviceClass) {
return (Class<? extends T>) serviceConfig.get(serviceClass);
}
然后利用反射找到Implement_$$_Database这个类,然后从方法中找到配对。

public class InjectHelper {

@Nullable
public static <T> T getInstanceByDatabase(Class<T> tClass) {
Implement_$$_Database database = new Implement_$$_Database();
Class<? extends T> implementClass = database.getServiceImplementClass(tClass);
if (implementClass != null) {
try {
return implementClass.newInstance();
} catch (Exception e) {
e.printStackTrace(www.yigouyule2.cn/);
}
}
return null;
}
然后在需要配置的地方添加注解即可。

@Implements(parent = IUserService.class)
class UserServiceImpl implements IUserService {

@Override
public String getUserName() {
return "UserServiceImpl.getUserName";

调用姿势。

binding.button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
IUserService userService = InjectHelper.getInstanceByDatabase(IUserService.class);
if (userService != null) {
Toast.makeText(getContext(), userService.getUserName(), Toast.LENGTH_SHORT).show();
}
}
});

注意点:

有时候,在生成最终的配置文件的时候,文件的名字是固定的,比如上面的Implement_$$_Database,最终的路径是这样的cn.mycommons.implements.database.Implement_$$_Database.java,然后通过编译到apk中或则是aar中。

但是有个问题,如果各个子模块都使用了这样的插件,那么每个子模块的就会有这个Implement_$$_Database.class,那么就会编译出错。

因为aar中包含的时候class文件,不是java文件,不能在使用APT做处理了。下面有2中解决方案。

子工程的插件生成的文件包含一定的规则,比如包含模块名字,如User_Implement_$$_Database.java,同时修改编译过程,把java文件也打包到aar中,主工程的插件在编译时候,提取aar中的文件,然后合并子工程的所有的代码,这个思路是可行的,不过技术实现起来比较麻烦。

同一的方式类似,也是生成有一定规则的的文件,或者在特地package下生成class,这些class再通过接下来的所讲的Gradle Transform方式,生成一个新的Database.class文件。

Gradle Transform

这是Android Gradle编译提供的一个接口,可以供开发自定义一些功能,而我们就可以根据这个功能生成映射匹配,这种方式和APT类似,APT是运行在代码编译时期,而且Transform是直接扫描class,然后再生成新的class,class中包含Map映射信息。修改class文件,使用的是javassist一个第三方库。

下面简单讲述代码实现,后面有机会单独写一篇文章讲解。

首先定义一个注解,这个注解用于标注一个实现类的接口。

package cn.mycommons.modulebase.annotations;

@Target(ElementType.TYPE www.22yigouyule.cn)
@Retention(RetentionPolicy.CLASS)
public @interface Implements {
Class parent();

一个测试用的接口以及实现类。

public interface ITest {
}

@Implements(parent = ITest.class)
public class TestImpl implements ITest {

定义一个静态方法,用于获取某个接口的实现类。

package cn.mycommons.modulebase.annotations;

public class ImplementsManager {

private static final Map<Class, Class> CONFIG = new HashMap<>();

public static Class getImplementsClass(Class parent) {
return CONFIG.get(parent);
如果不使用任何黑科技,直接使用Java技术,那么在定义时候需要主动的往CONFIG这个map中添加配置,但是这里我们利用transform,直接动态的添加。

定义一个ImplementsPlugin gradle插件。

public class ImplementsPlugin implements Plugin<Project> {

@Override
public void apply(Project project) {
AppExtension app = project.getExtensions().getByType(AppExtension.class);
app.registerTransform(new ImplementsTransform(project));
}
}
1
2
3
4
5
6
7
8
自定义的Transform实现。

public class ImplementsTransform extends Transform {

static final String IMPLEMENTS_MANAGER = "cn/mycommons/modulebase/annotations/ImplementsManager.class"
static final String IMPLEMENTS_MANAGER_NAME = "cn.mycommons.modulebase.annotations.ImplementsManager"
Project project

ImplementsTransform(Project project) {
this.project = project
}

void log(String msg, Object... args) {
String text = String.format(msg, args)

project.getLogger().error("[ImplementsPlugin]:${text}")
}

@Override
public String getName() {
return "ImplementsTransform"
}

@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return ImmutableSet.of(QualifiedContent.DefaultContentType.CLASSES)
}

@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return ImmutableSet.of(
QualifiedContent.Scope.PROJECT,
QualifiedContent.Scope.PROJECT_LOCAL_DEPS,
QualifiedContent.Scope.SUB_PROJECTS,
QualifiedContent.Scope.SUB_PROJECTS_LOCAL_DEPS,
QualifiedContent.Scope.EXTERNAL_LIBRARIES
)
}

@Override
boolean isIncremental() {
return false
}

@Override
void transform(TransformInvocation transformInvocation)
throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
long time1 = System.currentTimeMillis();
log(this.toString() + ".....transform")

TransformOutputProvider outputProvider = transformInvocation.outputProvider
outputProvider.deleteAll()

def classPool = new ClassPool()
classPool.appendSystemPath()

// 记录所有的符合扫描条件的记录
List<Entry> implementsList = []
// ImplementsManager 注解所在的jar文件
JarInput implementsManagerJar = null

// 扫描所有的文件
transformInvocation.inputs.each {
it.directoryInputs.each {
classPool.appendClassPath(it.file.absolutePath)
def dst = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(it.file, dst)

project.fileTree(dst).each {
String clazzPath = it.absolutePath.replace(dst.absolutePath, "")
clazzPath = clazzPath.replace("/", ".").substring(1)
if (clazzPath.endsWith(".class")) {
clazzPath = clazzPath.substring(0, clazzPath.size() - 6)
CtClass clazz = classPool.get(clazzPath)
// 如果class中的类包含注解则先收集起来
Implements annotation = clazz.getAnnotation(Implements.class)
if (annotation != null) {
implementsList.add(new Entry(annotation, clazz))
}
}
}
}
it.jarInputs.each {
classPool.appendClassPath(it.file.absolutePath)

if (implementsManagerJar == null && isImplementsManager(it.file)) {
implementsManagerJar = it
} else {
def dst = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.JAR)
FileUtils.copyFile(it.file, dst)

def jarFile = new JarFile(it.file)
def entries = jarFile.entries()

// 如果jar中的class中的类包含注解则先收集起来
while (entries.hasMoreElements()) {
def jarEntry = entries.nextElement()
String clazzPath = jarEntry.getName()
clazzPath = clazzPath.replace("/", ".")
if (clazzPath.endsWith(".class")) {
clazzPath = clazzPath.substring(0, clazzPath.size() - 6)
def clazz = classPool.get(clazzPath)
Implements annotation = clazz.getAnnotation(Implements.class)
if (annotation != null) {
implementsList.add(new Entry(annotation, clazz))
}
}
}
}
}
}

log("implementsManagerJar = " + implementsManagerJar)

Map<String, String> config = new LinkedHashMap<>()

implementsList.each {
def str = it.anImplements.toString();
log("anImplements =" + it.anImplements)
def parent = str.substring(str.indexOf("(") + 1, str.indexOf(")")).replace("parent=", "").replace(".class", "")
log("parent =" + parent)
log("sub =" + it.ctClass.name)

// 收集所有的接口以及实现类的路径
config.put(parent, it.ctClass.name)
}

log("config = " + config)

long time2 = System.currentTimeMillis();

if (implementsManagerJar != null) {
def implementsManagerCtClass = classPool.get(IMPLEMENTS_MANAGER_NAME)
log("implementsManagerCtClass = " + implementsManagerCtClass)

// 修改class,在class中插入静态代码块,做初始化
def body = "{\n"
body += "CONFIG = new java.util.HashMap();\n"

for (Map.Entry<String, String> entry : config.entrySet()) {
body += "CONFIG.put(${entry.key}.class, ${entry.value}.class);\n"
}

body += "}\n"
log("body = " + body)

implementsManagerCtClass.makeClassInitializer().body = body

def jar = implementsManagerJar
def dst = outputProvider.getContentLocation(jar.name, jar.contentTypes, jar.scopes, Format.JAR)
println dst.absolutePath

// 修改完成后,完成后再写入到jar文件中
rewriteJar(implementsManagerJar.file, dst, IMPLEMENTS_MANAGER, implementsManagerCtClass.toBytecode())
}

log("time = " + (time2 - time1) / 1000)
}

static boolean isImplementsManager(File file) {
return new JarFile(file).getEntry(IMPLEMENTS_MANAGER) != null
}

static void rewriteJar(File src, File dst, String name, byte[] bytes) {
dst.getParentFile().mkdirs()

def jarOutput = new JarOutputStream(new FileOutputStream(dst))
def rcJarFile = new JarFile(src)

jarOutput.putNextEntry(new JarEntry(name))
jarOutput.write(bytes)

def buffer = new byte[1024]
int bytesRead
def entries = rcJarFile.entries()

while (entries.hasMoreElements()) {
def entry = entries.nextElement()
if (entry.name == name) continue
jarOutput.putNextEntry(entry)

def jarInput = rcJarFile.getInputStream(entry)
while ((bytesRead = jarInput.read(buffer)) != -1) {
jarOutput.write(buffer, 0, bytesRead)
}
jarInput.close()
}

jarOutput.close()
}
}
具体代码可以参考这里

映射匹配总结

优点:

Map:简单明了,很容易入手,不会对编译时间产生任何影响,不会随着Gradle版本的升级而受影响,代码混淆时候不会有影响,无需配置混淆文件。
APT:使用简单,使用注解配置,代码优雅,原理是用代码生成的方式生成新的文件。
Transform:使用简单,使用注解配置,代码优雅,原理是用代码生成的方式生成新的文件,不过生成的文件的时期和APT不同,会编译时间产生少许影响。
缺点:

Map:在需要新添加映射的时候,需要手动添加,不然不会生效,代码不优雅。
APT:在编译时期生成文件,会编译时间产生少许影响,同时在不同的Gradle的版本中可能会产生错误或者兼容问题。需要配置混淆设置,不然会丢失文件。技术实现复杂,较难维护。
Transform:在编译时期生成文件,会编译时间产生少许影响,同时在不同的Gradle的版本中可能会产生错误或者兼容问题。需要配置混淆设置,不然会丢失文件。技术实现复杂,较难维护。
从技术复杂性以及维护性来看,Map > APT = Transform

从使用复杂性以及代码优雅性来看,Transform > APT > Map

开发调试技巧

Debug

上面介绍了很多关于模块化的概念以及技术难题,当模块化完成以后,再进行完成开发时候还是会遇到不少问题。不如原先代码在一起的时候很方便的进行代码调试。但是进行模块化以后,直接使用的是aar依赖,不能直接修改代码,可以使用下面技巧,可以直接进行代码调试。

在根目录下面创建一个module目录以及module.gradle文件,这个目录和文件是git ignore的,然后把对应的模块代码clone到里面,根目录的setting.gradlew apply module.gradle文件,如下所示,如果需要源码调试,则在module中添加对应的模块。然后在app的依赖中去掉aar依赖,同时添加项目依赖即可。当不需要源码调试好,再修改为到原先代码即可。

try {
apply from: "./module.gradle"
} catch (e) {

module.gradle

include ':ModuleShopping'
1
比如调试shopping模块

// common
compile 'cn.mycommons:base:1.0.0'
compile 'cn.mycommons:component:1.0.0'
compile 'cn.mycommons:service:1.0.0'

// biz
compile 'cn.mycommons:user:1.0.0'
compile 'cn.mycommons:order:1.0.0'
// compile 'cn.mycommons:shopping:1.0.0'

当然还有个更具技术挑战性方案,使用gradle插件的形式,如果发现root项目中包含的模块化的源码,则不适用aar依赖,直接使用源码依赖,当然这个想法是不错的,不过具有技术挑战性,同时有可能随着Gradle版本的升级,编写的gradle插件也要做相对于的兼容风险,这是只是简单提示一下。

容器设计

上面讲到的如果要调试代码时候,需要完整的运行的整个项目,随着项目的增大,编译时间可能变得很长。

我们可以做一个简单的,类似与主app模块一样,比如我是负责user模块的开发者,那么我只要调试我这个模块就行了,如果需要其他的模块,我可以简单的做一个mock,不是把其他的模块直接依赖过来,这样可以做到调试作用。等到再需要完整项目调试时候,我们在使用上面介绍的方式,这样可以节省不少开发时间。

还有一种实现调试的方式,比如上面的user模块,目录下面的build.gradle文件是这样的

apply plugin: 'com.android.library'

xxx

我们可以在gradle.properties中设置编译变参数isLibModule,当需要完整调试好,设置为isLibModule=false,这样我这个子模块就是一个apply plugin: 'com.android.application'这样的模块,是可以单独运行的一个项目

try {
if (isLibModule) {
apply from: "./build_lib.gradle"
}else{
apply from: "./build_app.gradle"
}
} catch (e) {
}
可能有时候还是需要单独的运行环境,android编译方式有2中,一种是debug,一种是release。当打包成aar的时候,使用的是release方式,我们可以把需要调试的代码全部放到debug中,这样打包的时候就不会把调试的文件发布到aar中。不过这种实现方式,需要对Android项目的目录有较高的认识,才可以熟练使用。

CI

上面介绍的各个模块需要单独到独立的git仓库,同时打包到单独的maven仓库,当开发完成后,这时候就需要进行打包,但这个是一个简单和重复的事情,所以我们需要一个工具来完成这些事情,我们可以利用CI系统来搞定这件事情,这里我推荐Jenkins,主流厂商使用jenkins作为CI服务器这个方案。

具体的步骤就是,需要对每个模块的git仓库做web hook,我们公司使用的是git lab,可以对git的各种操作做hook,比如push,merge,tag等。

当代码发送了变化了,我们可以发送事件到CI服务器,CI服务器再对各个事件做处理,比如user模块develop分支有代码变化,这个变化可能是merge,也有可能是push。我们可以把主项目代码和user项目的代码单独clone下拉,然后编译一下,确认是否有编译问题,如果有编译通过,那么在使用相关gradle命令发布到maven仓库中。

不管每次编译结果怎样,是成功还是失败,我们都应该把结果回馈给开发者,常见的方式是邮件,不过这个信息邮件方式可能很频繁,我们建议使用slack。

总结

模块化架构主要思路就是分而治之,把依赖整理清楚,减少代码冗余和耦合,在把代码抽取到各自的模块后,了解各个模块的通信方式,以及可能发生的问题,规避问题或者解决问题。最后为了开发和调试方便,开发一些周边工具,帮助开发更好的完成任务。

关于Android模块化我有一些话不知当讲不当讲的更多相关文章

  1. 云课堂Android模块化实战--如何设计一个通用性的模块

    本文来自 网易云社区 . 如何设计一个通用性的模块 前言 每个开发者都会知道,随着项目的开发,会发现业务在不断壮大,产品线越来越丰富,而留给开发的时间却一直有限,在有限的时间,尽快完成某个功能的迭代. ...

  2. android 模块化

    android 插件化 模块化开发(apkplug): http://blog.csdn.net/o1587790525/article/details/11891997 android 模块化环境搭 ...

  3. Android中Context详解 ---- 你所不知道的Context(转)

    Android中Context详解 ---- 你所不知道的Context(转)                                               本文出处 :http://b ...

  4. Android自动接听&挂断电话(包含怎么应对4.1以上版本的权限检

    一  前言 这两天要研究类似白名单黑名单以及手势自动接听的一些功能,所以呢,自然而然的涉及到怎么自动接听/挂断电话的功能了.对于自动接听这一块,android4.1版本及其以上的版本和之前的版本处理逻 ...

  5. Android中Context详解 ---- 你所不知道的Context

    转自:http://blog.csdn.net/qinjuning/article/details/7310620Android中Context详解 ---- 你所不知道的Context 大家好,  ...

  6. Android模块化编程之引用本地的aar

    随着项目越来越多,代码的复用就变得异常重要,这时候就要进行模块化编程,就是把一些通用的组件或者类库做成单独的模块,其他项目直接进行引用就好.针对Android开发最常见的就是Android Libra ...

  7. Android 模块化编程之引用本地的aar

    转: http://www.stormzhang.com/android/2015/03/01/android-reference-local-aar/ 随着项目越来越多,代码的复用就变得异常重要,这 ...

  8. 解决android模块化升级方法

    有关本机android升级版本必须是全apk更新安装,我们无法实现的一些模块化升级的解决思路: 本地人+web混合动力APP~ 查询详情,我们必须实现模块化升级,无论使用方法,我这样做.首页写在每个功 ...

  9. Android 模块化探索与实践

    首发于<程序员>杂志五月刊 一.前言 万维网发明人 Tim Berners-Lee 谈到设计原理时说过:"简单性和模块化是软件工程的基石:分布式和容错性是互联网的生命." ...

随机推荐

  1. 使用jquery执行ajax

    $.ajax():返回其创建的XMLHttpRequest对象 回调函数:如果要处理$.ajax()得到的数据,则应该使用回调函数!beforeSend:在发送请求之后调用,需要一个XMLHttpRe ...

  2. BEC listen and translation exercise 33

    In fact, if it is a really hot day, like the sort of weather we had last summer, you are advised to ...

  3. SSH Secure Shell 的使用

    一,安装Secure Shell ,之后会出现SSH Secure Shell client 和SSH Secure File Transfer client 两个快捷方式. 需要服务器的IP地址,用 ...

  4. C++中函数模版和普通函数的区别

    函数模版和同名普通函数在同一个作用域中,会优先调用那个函数? 函数模型在进行调用的时候会进行严格的类型匹配,而普通函数在调用的时候,会进行函数参数类型转换(前提是自动类型转换). 调用函数模版,本质是 ...

  5. CodeForces - 1017F. The Neutral Zone (数学+Bitset存素数+素数筛优化)

    Notice: unusual memory limit! After the war, destroyed cities in the neutral zone were restored. And ...

  6. ACM学习历程—Hihocoder 1233 Boxes(bfs)(2015北京网赛)

    hihoCoder挑战赛12 时间限制:1000ms 单点时限:1000ms 内存限制:256MB   描述 There is a strange storehouse in PKU. In this ...

  7. React 版 V2EX 社区( react & react-router & axios & antd ui)

    目录 项目简介 在线演示 截图演示 踩坑 项目简介(1/4) Github: https://github.com/bergwhite/v2ex-react 项目使用React.Reac-router ...

  8. Django_form补充

    问题1:  注册页面输入为空,报错:keyError:找不到password def clean(self): print("---",self.cleaned_data)     ...

  9. POJ3468(线段树区间维护)

    A Simple Problem with Integers Time Limit: 5000MS   Memory Limit: 131072K Total Submissions: 85502   ...

  10. Python:模块详解及import本质

    转于:http://www.cnblogs.com/itfat/p/7481972.html 博主:东大网管 一.定义: 模块:用来从逻辑上组织python代码(变量,函数,类,逻辑:实现一个功能), ...