仿bugtags实现App线上Crash监控及问题复现
实现线上crash监控,包含了手机堆栈信息和设备信息,及时上传,后台修复并启用推送进行热修复流程,重要的是要手机用户的使用步骤及现场截屏,以便更快的复现问题。
大致的流程如下:
欢迎关注我的公众号:
我们预览下结果:
这是携带设备信息的堆栈信息文件
这是用户的使用步骤文件
这是最终上传到服务端后的文件
这是发生crash时候的现场截屏
下面按照项目的实现过程介绍怎么具体实现发生fc之后及时上传。
1、首先初始化BugMonitor工具BugMonitor.init(this);及小米推送MiPushClient.registerPush(this, APP_ID, APP_KEY);和文件上传的工具Bmob.initialize(this,BMOB_APPID);(其实小米推送和文件上传可以自己后台实现,我是因为个人没有服务器,用的免费推送和文件存储服务,在此谢谢这些sdk公司)
@Override
public void onCreate() {
super.onCreate();
BugMonitor.init(this);
Bmob.initialize(this,BMOB_APPID);
MiPushClient.registerPush(this, APP_ID, APP_KEY);
LoggerInterface newLogger = new LoggerInterface() {
@Override
public void setTag(String tag) {
// ignore
}
@Override
public void log(String content, Throwable t) {
Log.d(TAG, "---->>t"+content, t);
}
@Override
public void log(String content) {
Log.d(TAG, "---->>content"+content);
}
};
Logger.setLogger(this, newLogger);
}
2、在BugMonitor中初始化CrashHandler,因为安卓中可以通过这个Handler来捕捉crash信息
public static void init(Context context) {
mContext = context;
initCrashHandler(context);
}
private static void initCrashHandler(Context context) {
Thread.UncaughtExceptionHandler exceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
CrashHandler crashHandler = new CrashHandler(context, exceptionHandler);
Thread.setDefaultUncaughtExceptionHandler(crashHandler);
}
3、CrashHandler 实现了 Thread.UncaughtExceptionHandler接口,并在uncaughtException方法中获取发生crash时的堆栈信息。
4、当发生crash的时候我们需要做的就是尽量收集有用的信息,下面我们仔细看下onCrash方法。
@Override
public void onCrash(String crashInfo) {
Activity activity = ActivityHelper.getActivity();
long mills = System.currentTimeMillis();
dirName = TIME_FORMATTER.format(mills);
Display defaultDisplay = activity.getWindowManager().getDefaultDisplay();
Point point = new Point();
defaultDisplay.getSize(point);
float density = activity.getResources().getDisplayMetrics().density;
String networkState = NetUtil.getNetworkState(BugMonitor.mContext);
PackageInfo info = null;
try {
info = BugMonitor.mContext.getPackageManager().getPackageInfo(BugMonitor.mContext.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
long freeMemory = MemoryUtil.getFreeMemory();
long totalMemory = MemoryUtil.getTotalMemory();
String cpuName = CPUUtil.getCpuName();
String curCpuFreq = CPUUtil.getCurCpuFreq();
String maxCpuFreq = CPUUtil.getMaxCpuFreq();
String minCpuFreq = CPUUtil.getMinCpuFreq();
int numCores = CPUUtil.getNumCores();
boolean rooted = SystemInfoUtil.checkSuFile();
activity.getWindow().getDecorView().setDrawingCacheEnabled(true);
Bitmap bmp = activity.getWindow().getDecorView().getDrawingCache();
BitmapHelper.saveBitmap(bmp, dirName);
final String s = CrashInfo.newInstance()
.setStackStrings(crashInfo)
.setScreenResolution(point)
.setDensity(density)
.setNetwork(networkState)
.setPackageInfo(info)
.setMemory(freeMemory,totalMemory)
.setCpuInfo(numCores,cpuName,curCpuFreq,maxCpuFreq)
.setIsRoot(rooted)
.flushString()
.toString();
saveLogToSDCard(s,"crashInfo");
String step = StepQueueHelper.flushString();
Log.e(TAG, "---->>step: "+step );
saveLogToSDCard(step,"step");
Log.e(TAG, s );
//1、将文件夹目录写入sp中,再次启动时读出其中的内容
//2、特殊情况:已启动就fc,根本来不及上传
Intent intent = new Intent(activity, UploadService.class);
activity.startService(intent);
}
5、首先我们获取当前的activity,截下当前界面的图片,并收集收集系统版本,api,屏幕分辨率,屏幕密度,内存情况,网络情况,cpu,是否root过等等信息,并通过一个Builder模式构建出一个String。同时,将用户的使用步骤队列输出为一个String。(这部分是核心,大家可以看代码https://github.com/lantier743865/kso)
6、关于使用步骤我们要看一下
package android.com.bugmonitor.base;
import android.com.bugmonitor.collector.BugMonitor;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.MotionEvent;
/**
* Created by wuxiaolong on 2018/3/9.
*/
public class BaseActivity extends AppCompatActivity {
private static final String TAG = "BaseActivity";
@Override
protected void onResume() {
super.onResume();
BugMonitor.onResume(this);
}
@Override
protected void onPause() {
super.onPause();
BugMonitor.onPause(this);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
BugMonitor.onDispatchTouchEvent(this,ev);
return super.dispatchTouchEvent(ev);
}
}
在这BaseActivity中我们记录了每个界面的进入退出,及事件分发情况。我们重点看事件这部分,因为这部分会找到对应的view的id。
@Override
public void onDispatchTouchEvent(Activity activity, MotionEvent event) {
if(event.getAction() == 0) {
this.lastX = event.getRawX();
this.lastY = event.getRawY();
} else if(event.getAction() == 1) {
float currentX = event.getRawX();
float currentY = event.getRawY();
if(this.lastX == currentX && this.lastY == currentY) {
View var5 = this.getView(activity.getWindow().getDecorView(), currentX, currentY);
Log.e(TAG, "---->>var5: "+var5 );
StepQueueHelper.enqueueStep(activity,var5);
}
}
}
其实,我们在获取到event后,可以通过当前的activity的decorView的位置算出当前是哪个view,这一点我也是从bugtags的源码看出来的,可以混淆过我看了半天才想明白。
public static void enqueueStep(Activity activity, View view) {
String path = view.getResources().getResourceName(view.getId());
String vName = path.substring(path.indexOf("/")+1);
String format = TIME_FORMATTER.format(System.currentTimeMillis());
stepBuilder.append(format)
.append(SPACE)
.append(activity.getClass().getName())
.append(SPACE)
.append(EVENT)
.append(VIEWID)
.append(vName)
.append(SPACE)
.append(TYPE)
.append(view.getClass().getName());
String s = stepBuilder.toString();
stepQueue.add(s);
stepBuilder.delete(0,stepBuilder.length());
}
有了view我们可以通过R文件获取它在我们项目中设置的id名称,这有助于我们找到对应的发生问题的空间。代码中一系列append是为了打印出来的信息格式化更容易分辨。
6、还有关键的一步时应用要挂掉了,怎么把信息传到服务端去?答案是另开一个service(在其他进程中)
Intent intent = new Intent(activity, UploadService.class);
activity.startService(intent);
关于BugMonitor关键的部分讲完了,剩下的就是代码编写和实现了。
其中upload是一个接口,大家可以用自己的服务器规范去实现,我个人用了bmob的免费存储。大致的目录结构是这样的:
7、我们看下最终的结果:
这是携带设备信息的堆栈信息文件
这是用户的使用步骤文件
这是最终上传到服务端后的文件
这是发生crash时候的现场截屏
最后总结:
- 工具是为了发生某些crash之后我们尽可能的多收集现场信息
- mipush和bmob是为了流程能够完善,最终我们还需要热修复来善后
- 代码地址:![https://github.com/lantier743865/kso]
- 写的不好,大家请见谅,以后会继续努力。