Android Pins 工程结构

Pins 工程结构能解决什么问题?

近期我们听到一些团队在做工程化方面的事情,其中都提到了Pins 工程结构,最先提出这个概念的是微信团队:微信Android模块化架构重构实践,在后来看到美团外卖也做了这个事情:美团外卖Android平台化架构演进实践

那Pins工程结构是什么?

上面这张图就是Pins工程结构。

那有什么用?或者能解决什么问题?

如果你的产品有多条业务线,每一期产品有上百个需求,各个业务线业务之间有非常多的交集。比如,一个业务线引用了五个业务线,其他业务线也是类似的引用,依次类推,相互引用不重复的话为5的5次方等于3125,如果业务线增长这个复杂度也是呈几何数增长,那我们在现有的工程环境下如何做呢?简单的方法就是都放在同一个Gradle Module 里面相互引用,各个业务线之间用包名做区分,但是各个包之间也是可以相互引用,久而久之就会发现,代码变成了一锅粥…变成一锅粥的后果也是显然的,不能独立拆分,代码合并非常容易冲突浪费时间等等,各种后果。下图是微信业务之间的引用情况,实际情况可能比这更糟糕。

Pins结构就较好的解决了上面的问题,各个业务线之间都是一个Pins模块,模块之间根据规定引用该引用的,这样业务线之间的代码边界就会比较清楚。

接下来我们看下如何实现,这里只提供下简单思路。

业务构建改造

1
2
3
4
5
6
7
8
9
10
11
12
sourceSets {
main {
def dirs = ['p_widget', 'p_theme',
'p_shop', 'p_shopcart',
'p_submit_order','p_multperson','p_again_order',
'p_location', 'p_log','p_ugc','p_im','p_share']
dirs.each { dir ->
java.srcDir("src/$dir/java")
res.srcDir("src/$dir/res")
}
}
}

上面的示例就简单实现了Pins结构,指定各模块到路径到srcDir,p_shop、p_shopcart等Pins模块构建时会合并到主工程。上面只是一个简单示例,实际情况可以做很多动态化控制,比如动态生成以及扫描当前路径下的Pins模块、根据配置动态合成Pins模块等等。下面是一个稍微复杂的例子。

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
def src_dir = new File(projectDir, 'src')
// 扫描当前模块下的Pins模块,并生成List
def p_module_names =
src_dir
.list()
.toList()
.stream()
.filter(
new Predicate<String>() {
@Override
boolean test(String name) {
return name == 'main' || (name.startsWith('p_') && new File(src_dir, name).isDirectory())
}
})
.collect(Collectors.toList())
// 把生成的List合成为srcDir格式
def p_src_dirs =
p_module_names
.stream()
.map(
new Function() {
@Override
Object apply(Object module) {
return ['src', module, 'java'].join('/')
}
})
.collect(Collectors.toList())

def p_res_dirs =
p_module_names
.stream()
.map(
new Function() {
@Override
Object apply(Object module) {
return ['src', module, 'res'].join('/')
}
})
.collect(Collectors.toList())
// 指定路径
sourceSets {
main {
manifest.srcFile "src/main/AndroidManifest.xml"
java.srcDirs = p_src_dirs
res.srcDirs = p_res_dirs
}
}

代码边界检查

上面只是表面上把代码进行了分割,但是各Pins模块还是可以引用到其他模块的代码,一般的操作是根据模块的配置,在编译期做代码检查,检查是否引用了不该引用的模块。

那如何定义项目的配置,这个配置可以是文本文件、DSL等等,微信通过project.properties来指定编译依赖关系。

这里简单用groovy格式文件举例:

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
task code_check {
doLast {
// 加载pins模块依赖文件
def dependenciesFile = new File(projectDir, 'src/p_module1/dependencies.groovy')
def ref = null
dependenciesFile.readLines().each {
ref = it
}
// 扫描pins模块内部源文件
File javaDir = new File(projectDir, 'src/p_module1/java')
Files.walkFileTree(javaDir.toPath(), new FileVisitor<Path>() {
@Override
FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE
}

@Override
FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
file.readLines().each {
if (it.endsWith(ref)) {
System.err.println("p_module1模块引用了不能引用的模块!")
return FileVisitResult.TERMINATE
}
}
return FileVisitResult.CONTINUE
}

@Override
FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE
}

@Override
FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE
}
})
}
}

定义一个任务,先从p_module1的dependencies文件里读出不能包含的模块,然后检索p_module1里面的文件是不是引用了这个模块,如果检查到就终止或抛出异常。

这里只是列举了一个思路,实现也比较粗暴,直接匹配的字符串。

当然,也可以做的比较完善,这些逻辑可以做在一个插件里,插件每次读取各个Pins模块的DSL配置(根据DSL的扩展性做更细粒度的依赖关系,比如只依赖另一个模块的某个包、某个类、某个资源等等),插件根据配置可以动态合成Pins,合成完Pins再做代码边界检查,边界检查可以用字符流匹配也可以用其他方式,提高字符流匹配准确性也可以做很多事情,比如匹配import行、类定义行、以及代码内的匹配(是真的字符串还是真的引用等等)。

Pins 工程的基本思路就是这样。

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2017-2023 Jacky

所有的相遇,都是久别重逢

支付宝
微信