捕获 flutter app的崩溃日志并上报

flutter 的崩溃日志收集主要有两个方面:

  1. flutter dart 代码的异常(包含app和framework代码两种情况,一般不会引起闪退,你猜为什么)
  2. flutter engine 的崩溃日志(一般会闪退)

Flutter App 代码异常捕获

人写的代码是无数异常交织起来的偶然产物,代码发生异常才是正常情况。

除了在关键的地方加上 try-catch 让它们变成已知异常之外,抓到未知异常才是真本事。

比如下面的一段代码中的try-catch是无效的:

1
2
3
4
5
try {
Future.error("asynchronous surprise");
} catch (e){
print(e)
}

好在,Dart 有一个 Zone 的概念,有点类似sandbox的意思。不同的 Zone 代码上下文是不同的互不影响,Zone 还可以创建新的子Zone。Zone 可以重新定义自己的printtimersmicrotasks还有最关键的how uncaught errors are handled 未捕获异常的处理

1
2
3
4
5
runZoned(() {
Future.error("asynchronous error");
}, onError: (dynamic e, StackTrace stack) {
reportError(e, stack);
});

reportError 里即可以进行上报处理(详见后面介绍)。

Flutter framework 异常捕获

注册 FlutterError.onError 回调,用于收集 Flutter framework 外抛的异常。

1
2
3
FlutterError.onError = (FlutterErrorDetails details) {
reportError(details.exception, details.stack);
};

该 error 一般是由 Widgetbuild 的时候抛出,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@override
void performRebuild() {
Widget built;
try {
built = build();
} catch (e, stack) {
built = ErrorWidget.builder(_debugReportException(ErrorDescription("building $this"), e, stack));
} finally {
_dirty = false;
}
try {
_child = updateChild(_child, built, slot);
} catch (e, stack) {
built = ErrorWidget.builder(_debugReportException(ErrorDescription("building $this"), e, stack));
_child = updateChild(null, built, slot);
}
}

简单几行代码给你的android应用生成更难以阅读的混淆字典

普遍的,不论大小Android应用都会配置 proguard 在release 编译的时候混淆自己的代码:

1
2
3
4
5
6
7
8
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

但无论 Proguard 还是 R8,他们的混淆字典默认都太简单了(too simple),只是 abcdefg 而已,反编译后还是很容易阅读的,如下所示:

1
2
3
4
5
6
7
8
final class b {
Object a;
aes b;
ada c;

b() {
}
}

所幸,Proguard 支持自定义字典:

1
2
3
-obfuscationdictionary dict.txt
-classobfuscationdictionary dict.txt
-packageobfuscationdictionary dict.txt

如果,有那么一个字典,里面都是形似“乱码”的字符,看起来不仅费眼睛,甚至电脑字体还没收录更佳(会显示成一个方框)。

万能的github 上还真有符合要求的。但是直接生成好的字典文件一直用也是有隐患的,举个例子,两个版本之间类、方法的个数差别不大,最终的混淆结果其实是很相似的,对比 mapping 之后,有可能一个方法前一个版本叫 aa,现在叫 ab 了。

而且翻看了部分实现方案,要么是字典文件里词汇量不够大,要么生成代码实现可能有其它bug。故而干脆自己撸起袖子几行代码搞定。

flutter run lost libflutter.so when using abiFilters

Reproduce

Add abiFilters to android/app/build.gradle :

1
2
3
4
5
6
7
android {
defaultConfig {
ndk {
abiFilters 'armeabi-v7a'
}
}
}

Run command:

1
$ flutter run

Flutter app crashed by java.lang.UnsatisfiedLinkError:

java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file “/data/app/xxxx/base.apk”],nativeLibraryDirectories=[/data/app/xxxx/lib/arm, /data/app/xxxx/base.apk!/lib/armeabi-v7a, /system/lib, /vendor/lib, /product/lib]]] couldn’t find “libflutter.so”
at java.lang.Runtime.loadLibrary0(Runtime.java:1011)
at java.lang.System.loadLibrary(System.java:1660)
at io.flutter.view.FlutterMain.startInitialization(FlutterMain.java:156)
at io.flutter.view.FlutterMain.startInitialization(FlutterMain.java:133)
at io.flutter.app.FlutterApplication.onCreate(FlutterApplication.java:22)

As you can see, flutter run seems like forgot to put “libflutter.so” into apk.

程序员是个热爱学习的群体

cat.jpg

譬如你跟他说某开源项目很好值得深入学习,他可能点个star表示已阅,或者点个fork表示可以深入研究,clone代码那是万万不可能的。

但假如在某程序员聚集地说某项目源码泄漏了,那景象好比炸窝,蜂拥而上,生怕吃不上热的,star+fork+clone素质三连,忙得不亦乐乎。

费劲周折毕竟拿到代码了,也不管看不看得懂,像是对着名画,总要品头论足一番,凭着一知半解发表几点高论。

开动鹰眼扫代码要是发现点奇特的地方,那如同发现新大陆,喜不自胜,更丝毫不吝啬自己的言辞到处去说。

程序员真的是个热爱学习的群体。

修改 Gradle 插件(Plugins)的下载地址(repositories)

Gradle 也可以用下面的方式声明使用的插件:

1
2
3
4
// build.gradle
plugins {
id 'com.example.plugin', version '1.0'
}

其实是从 Gradle 官方的插件仓库 https://plugins.gradle.org/m2/ 下载的。

但是,众所周知的原因,某些地区会连不上,导致下载不到需要的插件,例如出现如下错误:

1
2
3
4
5
6
7
* What went wrong:
A problem occurred configuring root project 'MyApp'.
> Could not resolve all files for configuration ':classpath'.
> Could not download jimfs.jar (com.google.jimfs:jimfs:1.1)
> Could not get resource 'https://plugins.gradle.org/m2/com/google/jimfs/jimfs/1.1/jimfs-1.1.jar'.
> Could not HEAD 'https://plugins.gradle.org/m2/com/google/jimfs/jimfs/1.1/jimfs-1.1.jar'.
> Connect to d29vzk4ow07wi7.cloudfront.net:443 [d29vzk4ow07wi7.cloudfront.net/54.192.84.6, d29vzk4ow07wi7.cloudfront.net/54.192.84.168, d29vzk4ow07wi7.cloudfront.net/54.192.84.128, d29vzk4ow07wi7.cloudfront.net/54.192.84.173] failed: Read timed out

又或者,插件是不对外的,存在某个私有仓库的,该如何修改或者添加额外的私有仓库地址呢?

如何简单快速搭建 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 假便去舟山的枸杞岛走了一遭。

三礁江大桥

~


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

~