怎么实现微信Android Pins工程结构?

9,108 阅读4分钟

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

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

那Pins工程结构是什么?

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

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

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

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

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

业务构建改造

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模块等等。下面是一个稍微复杂的例子。

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格式文件举例:

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 工程的基本思路就是这样。