出色的多平台开发 SDK - Fabric

前言

不久前我曾经说过要做个回顾(嗯,这很不久),我就先从应用选用的工具说起吧.

千里之堤,溃于蚁穴 - 韩非《韩非子·喻老》

为了写一个质量较好的应用,一个趁手的数据/崩溃统计 SDK 是必不可少的.

其实国内友盟这些做的还是不错的,只是太臃肿了(大而全的后果),最新的 6.0.4 就达到 1.5MB 了,我的应用才 2M 不到,也不需要那么多的功能.

而且我更希望使用 Gradle 来管理和更新这些东西,这也避免了 SDK 版本需要更新的麻烦(还是懒).

最后我选择了 Twitter 带来的 Fabric.

Fabric
Free~

Fabric 支持 Android/IOS/Unity (纪念在天国的 Windows Phone),它可以根据你的需求来自定义模块,而且集成也是十分简单的.

Fabric
Fabric 的模块们

目前我使用的只有两个模块: Crashlytics 和 Answers. 前者用来检测应用崩溃和分发 Beta 应用,后者用来统计数据(用户量/事件等).

安装和使用

因为我写的是 Android 应用,所以就只有 Android 版本的了. 其他平台步骤应该差不了多少.

导入依赖

Android Studio/IntelliJ IDEA

步骤十分简单:

  1. 在 IDE 中打开 Plugins 选项(Settings->Plugins/Ctrl+Shift+A then type plugins)
  2. 选择 Browse repositories,在搜索框中输入 Fabric for Android Studio 并安装重启
  3. 打开 Fabric 并登陆账号
  4. 一路下一步->选择你要的模块->一路下一步
  5. 完成,我真是个天才!

Fabric
Fabric 的位置,你也可以 Ctrl+Shift+A then type fabric 来开启它.

Fabric
你可以直接在这里添加你需要的模块

Fabric
应用异常查看(我都不好意思发这张图了哈哈)

在 IDE 中你可以简略的查看应用的崩溃信息,详细信息还是要去网页和客户端看的.

查看信息

在网页端你能详细的查看各类信息.

Fabric
即时的统计信息

Fabric
各类事件统计

Fabric
崩溃详细信息

使用

手动初始化

要使用 Fabric,我们需要:

  • fabric.properties 中添加你应用的 apiSecret.
  • 更新 Module 级别的build.gradle.
  • 在应用的 Application 类中将其初始化.

build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
buildscript {
repositories {
maven { url 'https://maven.fabric.io/public' }
}

dependencies {
classpath 'io.fabric.tools:gradle:1.+'
}
}
repositories {
maven { url 'https://maven.fabric.io/public' }
}

apply plugin: 'io.fabric'
...

dependencies {
...
compile('com.crashlytics.sdk.android:crashlytics:[email protected]') {
transitive = true;
}
}

Application

1
2
3
4
5
6
7
8
9
public class YooooEXApplication extends Application {
...
@Override
public void onCreate() {
super.onCreate();
Fabric.with(this, new Crashlytics());
...
}
}

fabric.properties

1
2
#Contains API Secret used to validate your application. Commit to internal source control; avoid making secret public.
apiSecret=apiSecret
自动初始化

如果你按照 导入依赖 一路走下去的话,Fabric就已经自动初始化完毕了. (把这个放在后面是因为我想让你们看看手动配置是很麻烦的一件事情哈哈哈哈哈哈哈)

至此, Fabric 就已经完成初始化并开始收集数据了.

自定义提交数据

手动提交异常

Fabric 默认的设置已经能满足我的大部分需求,但是不折腾一下还是不行滴(不然写这个东西干嘛呢).

一般来说我们对可遇见的程序异常都会捕捉起来并返回错误信息给用户,但对于不可预料的异常便无能为力了. 下面是我在 Feeder 中对网络请求发生异常的一段处理代码:

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
/**
* 获取错误信息
*
* @param context 上下文
* @param e 异常
* @return 错误信息
*/
public static String getExceptionMessage(Context context, Throwable e) {
Log.d(TAG, "getExceptionMessage: " + e.getClass());
String message = context.getString(R.string.err_unknow);
try {
if (e.getClass() == HttpException.class) {
String errorBody = ((HttpException) e).response().errorBody().string();
switch (((HttpException) e).code()) {
case HttpURLConnection.HTTP_UNAUTHORIZED:
if (TextUtils.equals(errorBody, NetworkUtils.ERROR_LOGIN_FAILED)) {
message = context.getString(R.string.err_wrong_credential);
} else {
message = context.getString(R.string.err_user_token_expired);
}
break;
case HttpURLConnection.HTTP_BAD_GATEWAY:
message = context.getString(R.string.err_proxy);
}
} else if (e.getClass() == SocketTimeoutException.class ||
e.getClass() == SSLHandshakeException.class ||
e.getClass() == UnknownHostException.class ||
e.getClass() == SSLException.class) {
message = context.getString(R.string.err_network_timeout);
} else {
Crashlytics.log("An unexcepted exception occurred. Context: " + context.toString());
Crashlytics.logException(e);
}
} catch (IOException e1) {
e1.printStackTrace();
}
return message;
}

Feeder 的网络请求是通过 Retrofit + RxJava 来实现的,当发生异常时,这段代码会判断异常是不是网络请求抛出的. 如果是 HttpException 则展示相对应的错误信息给用户. 当没有相对应的异常时就会返回一个”未知错误”信息给用户.

在这里我已经对可预见的网络异常加入了判断语句当中,但当发生了意料之外的异常时, Fabric 中的 Crashlytics 模块就能起到作用了.

我们看向 31-32 行:
Crashlytics.logException(e);
Crashlytics.log("An unexcepted exception occurred. Context: " + context.toString());

在这里我调用了 Crashlytics.logException(Throwable throwable)Crashlytics.log(String msg) 这两个方法.当发生意料之外的网络异常时, Fabric 会向服务器提交这个异常,并附上了相应的 Context 来帮助我更快速的定位到问题所在.

当然, Crashlytics 还能提交变量,帮助你更详细的分析异常原因:

1
2
3
4
5
6
7
8
9
Crashlytics.setString(key, value);
Crashlytics.setBool(String key, boolean value);
Crashlytics.setDouble(String key, double value);
Crashlytics.setFloat(String key, float value);
Crashlytics.setInt(String key, int value);
Crashlytics.setLong(String key, long value);
Crashlytics.setUserIdentifier(String identifier);
Crashlytics.setUserName(String name);
Crashlytics.setUserEmail(String email);

提交统计信息

和异常一样,要想让 Fabric 收集统计你自定义的数据,你还需要运用好 Answers 模块.

Answers 默认会统计的手机型号和系统版本等信息,如果你想要更详细的事件统计信息(用户登录/注册/分享内容等),你就需要自定义一些东西来满足你的需求了.

还是拿 Feeder 来做例子,来一个登录事件的统计吧.

用户登录

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

public class LoginPresenterCompl implements LoginPresenter {
private static final String TAG = "LoginPresenterCompl";
private final LoginView view;
private final Context loginContext;

...
//来自 LoginPresenter 接口的说明
/**
* 用户登录-客户端方式
*
* @param email 邮箱
* @param password 密码
*/
@Override
public void login(String email, String password) {
...
@Override
public void onError(Throwable e) {
view.dismissProgressDialog();
String reason = CommonUtils.getExceptionMessage(loginContext, e);
view.showMessage(reason);
}
...
@Override
public void onCompleted() {
if (TextUtils.isEmpty(UserData.getToken())) {
view.loginResult(false);
} else {
getUserInfo();
}
}
...
}

登录事件完成后的回调

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void loginResult(boolean flag) {
if (flag) {
Answers.getInstance().logLogin(new LoginEvent()
.putMethod("Client")
.putSuccess(true));
startActivity(new Intent(this, MainActivity.class));
finish();
}
Answers.getInstance().logLogin(new LoginEvent()
.putMethod("Client")
.putSuccess(false));
}

可以看见,在用户点击登录后,程序开始向服务器进行验证,当验证完毕后,根据验证结果调用 loginResult(flag).

loginResult(flag) 中则根据结果调用了 Answers

1
2
3
Answers.getInstance().logLogin(new LoginEvent()
.putMethod("Client")// Login method
.putSuccess(flag));// Login status

当调用此方法后,Answers 模块就会向服务器提交事件信息,你就可以得知用户的登录状况统计了.

想要自定义事件也是比较简单的,如果你只是在一个地方提交这个事件,你可以使用 CustomEvent:

1
2
3
Answers.getInstance().logCustom(new CustomEvent("EventName")
.putCustomAttribute("key", "value")
.putCustomAttribute("key",Number number)

如果该事件在多个地方都要提交,你可以把它封装好或照着 Answers 中官方提供的事件写一个.

参考连接: