如何简单快速搭建 Android 大仓

书接上文,上回提到 B 站Android团队为了解决组件化后协作上的问题,已经采用了大仓(monorepo)的方案来组织代码。

国内实践大仓的团队少之又少,更别提 Android 的大仓了,几乎没有来自其它团队的可借鉴经验。在这条路上,我们可以算作先行者。本文粗陋,文中所列思路不可能适用所有团队,仅给同样想实践Android 大仓的人些许启发。

一个标准的 Gradle 项目

首先回顾一下 Android 项目的组织方式。自从13年开始官方逐渐迁移到 Android Studio 做为 IDE 后,Android 项目的开发和编译就绑在 Gradle 上了。

一个标准的 Gradle 项目结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MyApp/
├── build.gradle
├── settings.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradle.properties
└── app
├── build.gradle
└── src
└── main
├── java
├── res
└── AndroidManifest.xml

通常,会有多个Gradle Module存在:

1
2
3
4
5
6
7
8
9
10
11
12
MyApp/
├── build.gradle
├── settings.gradle
├── app
│ ├── build.gradle
│ └── src
├── lib1
│ ├── build.gradle
│ └── src
└── lib2
├── build.gradle
└── src

其中 settings.gradle 会注册所有的 Module

1
include ':app', ':lib1', ':lib2'

多仓库

随业务的扩张,Module 数量会越来越多。遵循多数人实践过的组件化的思路,按业务分仓库存放便理所当然:

1
2
3
4
5
6
7
8
9
10
11
12
13
android group/
├── MyApp/
│ ├── build.gradle
│ └── settings.gradle
├── app1/
│ ├── build.gradle
│ └── settings.gradle
├── app2/
│ ├── build.gradle
│ └── settings.gradle
└── libs/
├── build.gradle
└── settings.gradle

每个仓库都是一个标准 Gradle 项目,通过 publishing 插件将module 都上传 aar(或者jar)到 maven私服(如nexus)上,再在 MyApp/build.gradle 中以 maven 组件的形式依赖它们,最终打包成apk:

1
2
3
4
5
6
7
8
9
10
11
repositories {
maven {
name = "myRepo"
url = "http://myrepo.example.com/android"
}
}
dependencies {
implementation 'com.example.android:app-a:1.0.0'
implementation 'com.example.android:app-b:1.0.0'
implementation 'com.example.android:lib-a:1.1.0'
}

此时的代码组织方式便是上文中所述的多仓库形态(可能许多团队正处于当前阶段)。

多仓到大仓

那么,如何既能快速搭建出适用于 Android 的大仓,又能不影响当前的团队协作流程,还要尽量避免迁移带来的开发效率降低?

B 站 Android 代码库的演进历程

早在2012年,B 站 Android APP 便已上线。当时开发者不过一人,而如今,业务线众多、隶属不同团队的Android 端开发人员数以百计。从单兵作战到百花争鸣,代码库的组织管理也随之经过数次的改革、演进。

单仓库

2014年底,Android 端的常驻开发人员一只手也数的过来。业务发展迅速,为追求效率,方便管理,所有代码都在一个仓库中,甚至包括第三方的、开源的代码(个别用 git submodule 管理)。Clone下来导入 Eclipse 就可以开干。

到大约15年中旬,开始使用 Android Studio,得益于 Gradle 的项目管理理念,分出了多个 library module。外部依赖使用 maven。也是这一期间开始搭建了内网 maven 服务。

这期间代码库组织结构是:单仓库 + 个别 git submodule

这种组织方式好处显而易见:

  1. 项目结构简单:随时随地 clone 下来导入 IDE 即可以开始开发,代码所有人可见,没有额外的限制。
  2. 方便快速迭代:改动可以快速入库,适合小团队,review方便,改动透明

但是,约莫到16年中,业务发展,新团队纷纷成立,招聘要求降低人员迅速膨胀。这种小而美的代码库已经不适用了,主要有以下缺点:

  1. 代码结构混乱,模块之间依赖关系混沌:倒不如说是技术债,前期的疲于业务迭代,以及没有及时的规划出好的代码层级架构,如今人员纷杂水平不一,之前追求的“没有限制”反而诱发了恶果
  2. 编译时间变长:业务增速发展,代码量爆炸式增长,单机编译越来越慢,开发幸福感跌到谷地

Hexo 使用 DisqusJS 代理评论

博客目前用的是Hexo,没有后端,为静态博客,评论一般用的第三方系统,如常用的 Disqus。但众所周知的原因,在“火星”上无法正常访问Disqus。

隆重推荐一个通过api实现评论的项目:DisqusJS

这里说一下它的配置技巧。

添加DisqusJS 到 hexo主题

这里以很多人用的 Next 主题为例,其它类似。

修改_config.yml 中的disqus配置为:

1
2
3
4
5
6
7
8
# for DisqusJS, https://github.com/SukkaW/DisqusJS
disqus:
enable: true
shortname: #你的shortname
api: #https://disqus.com/api/
apikey: #你申请的public api key
admin: #你的名字
adminLabel: #你的特别标识

具体含义见DisqusJS

修改 layout/_partials/head/custom-head.swig

1
2
3
{% if theme.disqus.enable %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/disqusjs@0.2.5/dist/disqusjs.css">
{% endif %}

让 Terminal 和 Android Studio 使用同一个 Gradle Daemon

在同时使用 Terminal 和 Android Studio 开发和编译 Android 项目时,跑 gradle 任务经常有一个提示,类似:

Starting a Gradle Daemon, 2 incompatible Daemons could not be reused, use --status for details

这表示 Gradle 又开启了一个新的 Daemon,已经存在的没有被重用。

执行gradlew --status可以看到已经有3个Daemon 存活,对于我这台13年末产的老爷机来说是个严重的负担

1
2
3
4
5
6
7
$ ./gradlew --status
PID STATUS INFO
33346 IDLE 4.6
31842 IDLE 4.6
29669 IDLE 4.6

Only Daemons for the current Gradle version are displayed. See https://docs.gradle.org/4.6/userguide/gradle_daemon.html#sec:status

存在多个 Daemon 的原因

Gradle 官方给的解释是:

  1. Gradle 版本不一样,比如一个项目用了 Gradle 4.4 而另一个用了 Gradle 4.6;
  2. Java 版本不一样,比如一个项目使用 JDK 7 而另一个则是 JDK 8 执行的 Gradle;
  3. Daemon JVM 参数不一样,比如一个项目指定了 -Xmx1024m 而另一个 -Xmx2048m
  4. 存在的 Daemon 都处于 BUSY 状态,比如Android Studio 和 Terminal 同时跑 Gradle 任务。

如何共用一个Daemon

针对上述原因,可以得到以下方案:

对于同一个项目来说,使用同一个 JDK 执行 Gradle即可;如果是多个项目则另外需要指定使用同一个版本的gradle、并设置同样的 JVM 参数。

瞎拍(5)- 枸杞岛之旅

大王村沙滩

一直存着愿,想去看碧海蓝天,趁着 5.1 假便去舟山的枸杞岛走了一遭。

三礁江大桥

~


可惜天公不作美,小岛上一直有雾罩着,能见度不高,幻想中的海天一色,终究没看着。

~

瞎拍(4)

老家门口的油菜花开的正盛,花丛中许多蜜蜂穿梭,忍不住追着瞎拍了几张 (´・ω・`)

✿

✿

✿

✿

✿

一个小插件解决组件化引发的DEX字段数爆炸的问题

TL;DR

插件名:shrinker

项目地址: https://github.com/yrom/shrinker(其实很早之前就已经发布到github上了,不过无人问津→_→)

插件效果:与removeUnusedCode 同用可以起到最佳效果

这里有一个简单的测试项目,大部分类来自于依赖的support库,结果如下:

选项methodsfieldsclasses
原始项目22164143672563
应用shrinker 插件2197978052392
应用shrinker 并开启 removeUnusedCode1133533021274

如果应用于依赖众多的大型项目则效果惊人。

ps. 其实已经在 b站的APP 上使用很久了,插件稳定、可靠且无副作用。

原理

不论组件化或者说模块化,都有个核心思想:拆分,拆成一个又一个独立的Library。

拆分 Library 引入的问题

举个例子

现一个 APP,它为了实践组件/模块化,拆分出了 common-ui ,business-a, business-b… 依赖关系如下图所示:

R 文件生成的大致流程如下图:

其中processReleaseResources 实际是调用的 aapt工具来给每个依赖的Library都生成一个最终确定的R.java

可想而知,第一个问题:** 拆分的Android Library越多,R 文件越多! **

然而,Library 的 R 文件只会在最终编译成 APK 时确定字段常量值,输出 aar 时只有一个R.txt用于记录声明的资源。

假设 common-ui 声明了15个公共drawable资源,则生成的 R 文件中将有 15个相关的用于记录的字段,而且每个依赖于它的上层的library 生成的R都会有这15个同名的字段,如下图:

由此可得,第二个问题:** 越底层的依赖所声明资源越多,最终生成的 R 文件越庞大 ! ** 因为这些字段没有得到有效内联,最终生成的DEX字段数就会严重超标。

为了解决组件/模块化进程中出现的上述两个问题,shrinker 应运而生。

《Android Kotlin 指南》中译版

虽然很可惜没有在Kotlin 刚发布的时候跟进学习,现在也还不晚~

官方很早在 Github 上发布了一个名为 android kotlin guides 的网站供 Android 开发者学习使用,但似乎没有中文版(至今没找到╮( ̄▽ ̄)╭)。

于是乎,自己动手丰衣足食。也就几个页面,将它们都翻译了一下~

链接:https://yrom.github.io/kotlin-guides/
项目:https://github.com/yrom/kotlin-guides/

又试着将一个小工具用kotlin 重写了一遍,用以实践kotlin:https://github.com/Bilibili/xpref

That’s ALL.

瞎拍(3)

又闲来无事到植物园瞎拍了几张,也不枉这秋高气爽ヽ(゚∀゚*)ノ

✿

✿

✿

✿

Re: 用Go实现渠道打包工具walle

Walle,是美团点评技术团队针对 Android Signature V2 Scheme 签名过的APK的一个多渠道打包工具

近来恰巧在学习Go,就想着把它“翻译”成Go,权当作练手,同时还能给“包管理后台”用用。

Walle 的工作原理

想要把一个工具用另一种语言实现,首先得明白它的原理。

其实原理官方已经解释的很详细了,这里就不再详细缀述,主要列出一些关键的点:

APK 签名的本质

APK 文件其实是一个 ZIP(Jar包),所谓签名其实就是对包或者包中的文件做签名

V1 其实就是 Jar 签名,“签名”要保护的是包中的文件不被修改,并不保护包本身(如ZIP Comment);V2 则是针对V1的缺陷,在V1的基础上将包本身视为Blob再做一次签名。

这就要求 V2 需要在不破坏、不影响 ZIP 文件格式(能被普通的ZIP工具解压),不污染V1签名正确性(APK能被老Andriod系统识别)的前提下写入到包中。
APK 签名验证过程
(图来自https://source.android.com/security/apksigning/)
故而需要针对性的在不伤害 ZIP 文件格式的前提下做一些“魔改”,将 V2 签名数据块插入到 ZIP 的 Central Directory 之前。
APK V2 签名之前与之后

V2 签名之后的APK

APK 被分为 4 部分:

  1. Contents of ZIP entries (从文件头开始直到 APK Signing Block)
  2. APK Signing Block
  3. ZIP Central Directory
  4. ZIP End of Central Directory

1、3、4的内容受到APK Signing Block 保护,准确的说是受 APK Signing Block 中的签名信息保护

APK Signing Block 格式

  • Signing Block 字节数 (不含自身计数) (uint64)
  • “ID-value” 对序列:
    • 此“ID-value” 长度 (uint64)
    • ID (uint32)
    • value (可变长度: “ID-value” 长度 - 4个字节)
  • Signing Block 字节数 (与前面的数值相同) (uint64)
  • magic “APK Sig Block 42” (16 个字节)

为向后兼容考虑,ID-value对设计成了可以存在多个。

V2 签名是数据块中的一个ID-value对,其ID0x7109871a,值即为“签名信息”(signed data)。

在验证 V2 签名过程中,会跳过验证器不认识的 ID

插入渠道信息

从上面的原理可以得到结论APK Signing Block中可以增加一个ID-value来存储渠道,且不会破坏原有签名信息,即毋须重签名做到插入渠道