稳定性优化
稳定性中两个常见场景:Crash和ANR
提高代码质量
代码审查
-
何时审查
分两个方面:一是这个模块是否需要审查,明确代码审查的必要性,二是在开发阶段的哪个时间点代码审查,确定审查合理时间点。
一般审查:底层公共模块、重大特性业务代码、与其他模块有耦合、新手、应用即将发布前的紧急修改。
- 谁来审查
代码审查分为三种方式
- 团队审查:底层通用模块
- 模块负责人审查:某些模块化的功能和业务
- 结对审查:两人结对互相审查
- 审查内容
审查的流程:先审查设计实现思路,然后审查设计模式,接着审查形成的骨干代码,然后审查完成的代码。
审查内容:
- 实现思路和设计思想
- 代码设计
- 设计逻辑
- 代码风格
- 需求理解
代码静态扫描工具
4种常用Java代码分析工具对比如下
- Checkstyle:通过对代码编码格式、命名约定、Javadoc、类she j设计等方面进行代码规范和风格的检查。
- FindBugs:通过检测类文件或JAR文件,将字节码与一组缺陷模式进行对比从而发现代码缺陷,完成静态代码分析。
- PMD:通过其内置的编码规则对Java代码进行静态检查,主要检查潜在的bug、未使用的代码、重复的代码、循环体创建新对象等问题。
- Android Lint:除了代码缺陷,还能检测代码布局的合理性。
Crash监控
Android应用中发生的crash有两种类型,Java层的Crash和Native层的Crash
Java层的Crash监控
Android中,Java虚拟机为每个进程都设置类一个默认的UncaughtExeptionHandler,用于处理本进程中未被try catch的Exception。因此只有实现UncaughtExeptionHandler接口,并进程启动时调用Thread.setDefaultUncaughtExceptionHandler(...)传入自定义的UncaughtExeptionHandler,发生未捕获异常时就会回调uncaughtException(Thread thread, Throwable ex) 方法。
demo
public class ABLCrashHandler implements UncaughtExceptionHandler {
public static final String TAG = "ABLCrashHandler";
// ABLCrashHandler 实例
private static ABLCrashHandler INSTANCE = null;
// 程序的 Context 对象
private Context mContext;
// 系统默认的 UncaughtException 处理类
private UncaughtExceptionHandler mDefaultHandler;
// 用来存储设备信息和异常信息
private Map<String, String> infos = new HashMap<String, String>();
//保证只有一个 ABLCrashHandler 实例
private ABLCrashHandler(Context context) {
this.init(context);
}
//获取 ABLCrashHandler 实例 ,单例模式
public static ABLCrashHandler getInstance(Context context) {
if (INSTANCE == null) {
INSTANCE = new ABLCrashHandler(context);
}
return INSTANCE;
}
/**
* 初始化
*
* @param context
*/
public void init(Context context) {
mContext = context;
// 获取系统默认的 UncaughtException 处理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
// 设置该 ABLCrashHandler 为程序的默认处理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 当 UncaughtException 发生时会转入该函数来处理
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
// 如果用户没有处理则让系统默认的异常处理器来处理
mDefaultHandler.uncaughtException(thread, ex);
} else {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Log.e(TAG, "error : ", e);
}
// 退出程序
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(1);
}
}
/**
* 自定义错误处理,收集错误信息,发送错误报告等操作均在此完成
*
* @param ex
* @return true:如果处理了该异常信息;否则返回 false
*/
private boolean handleException(final Throwable ex) {
if (ex == null) {
return false;
}
ex.printStackTrace();
//使用 Toast 来显示异常信息
new Thread() {
@Override
public void run() {
Looper.prepare();
try {
ABLApplication.getApplication().getServicesManager().stopScan();
ABLApplication.getApplication().getServicesManager().destroy();
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, String> entry : infos.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
sb.append(key + "=" + value + "\n");
}
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String message = writer.toString();
Log.i(TAG + ".handleException.message", message + " |");
String exceptionCode = ABLExceptionCoder.getExceptionCode(message);
Log.i(TAG + ".handleException.exceptionCode", exceptionCode + " |");
Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出,错误码:" + exceptionCode, Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
}
Looper.loop();
}
}.start();
// 收集设备参数信息
collectDeviceInfo(mContext);
// 保存日志文件
saveCrashInfo2File(ex);
return true;
}
/**
* 收集设备参数信息
* @param ctx
*/
public void collectDeviceInfo(Context ctx) {
try {
PackageManager pm = ctx.getPackageManager();
PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
String versionName = pi.versionName == null ? "null" : pi.versionName;
String versionCode = pi.versionCode + "";
infos.put("versionName", versionName);
infos.put("versionCode", versionCode);
infos.put("deviceID", Utils.getDeviceID_B());
}
} catch (NameNotFoundException e) {
Log.e(TAG, "an error occured when collect package info", e);
} catch (Exception e) {
Log.e(TAG, "an error occured when collect package info", e);
}
Field[] fields = Build.class.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
infos.put(field.getName(), field.get(null).toString());
Log.d(TAG, field.getName() + " : " + field.get(null));
} catch (Exception e) {
Log.e(TAG, "an error occured when collect crash info", e);
}
}
}
/**
* 保存错误信息到文件中
*
* @param ex
* @return 返回文件名称,便于将文件传送到服务器
*/
public String saveCrashInfo2File(Throwable ex) {
try {
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, String> entry : infos.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
sb.append(key + "=" + value + "\n");
}
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String result = writer.toString();
String exceptionCode = ABLExceptionCoder.getExceptionCode(result);
sb.append("exceptionCode=" + exceptionCode + "\n");
sb.append(result);
try {
String fileName = "crash-" + DateUtil.getNowTime3() + ".log";
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String path = FileUtils.getSDCardPath() + ABLConfig.SAVE_FOLDER + "/Bug/";
File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(path + fileName);
if (!file.exists()){
file.createNewFile();
}
//增加异常信息
FileWriter fw = new FileWriter(file, true);
fw.write("time:" + DateUtil.getNowTime());
fw.write("\r\n");
fw.write(sb.toString());
fw.write("\r\n\r\n");
fw.flush();
fw.close();
}
return fileName;
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "an error occured while writing file...", e);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
Nativa 层Crash监控
当应用程序发生异常时,Linux内核会生成错误信号并通知当前进程。应用进程接受到错误信号后,可以捕获该信号并执行对应的信号处理函数。而当应用发生验证错误时后发生Crash,Linux有一类专门用于描述Crash的信号。只要在应用程序注册了这些信号的处理函数,当JNI crash时,我们的处理函数就会被调用到,然后获取dump文件再上传,后续的工作就和Java异常逻辑一致了。
ANR剖析
ANR介绍
类型:
- KeyDispatchTimeout:对输入事件5秒内无响应
- BroadcaseTimeout:在指定时间(默认10秒)无法处理完毕,并且没有结束执行onReceive
- ServiceTimeout:指Service在特定时间(默认20秒)内无法处理完成。
ANR分析
如果发送ANR, Logcat会产生对应的日志和一个traces文件,这个文件保存在/data/anr/traces.txt。
可以直接用adb工具获取traces.txt文件:
adb pull /data/anr/traces.txt
可以通过分析log和traces文件信息定位到anr发生的位置和分析原因。
AS提供了一个分析trace文件的工具:Analyze Stacktrace。打开Analyze Stacktrace工具,将traces.txt文件内容复制到窗口,单击Normalize按钮,生成Thread Dump列表。如果某个线程被标红,说明此线程被堵塞了。
提高后台进程存活率
当内存紧张时,优先级低、占用内存大的app进程会优先被杀死。可以提高提高进程优先级使应用在后台的存活时间更长,一般使用以下几种方法实现:
- 网络连接
通过长连接心跳和进程保持通信,使进程保持活动状态,但如果系统内存非常紧张,也有可能被杀。
- 利用系统现有机制
一般可以注册系统消息(AlarmReceive、BootReceive等),通过系统消息响应挂起进程。
- SyncAdapter
利用Android系统提供的账号同步机制实现进程优先级提高。
SyncAdapter是一个系统服务,通过系统的定时器更新应用程序数据ContentProvider,因为Sync服务工作在独立进程,并由系统调度,属于核心进程级别,系统不会杀掉,而使用了SyncAdapter的进程优先级也会提高,优先级变为1,仅低于前台正在运行的进程,因此可以降低应用被系统杀掉概率。