背景
在日常开发和调试过程中,我们经常需要用到logcat的日志来帮助我们定位问题,在生产环境也有类似的需求,当程序出现崩溃、anr等异常时,我们除了需要捕获异常日志外,往往需要抓取一段完整的logcat日志,给我们研发同学去分析定位问题,但是生产环境是复杂的,如果当发生异常后,设备很有可能被重启了,这时候我们的logcat日志往往就丢失了,无法事后再去抓取,所以这时候就需要我们在程序中提供一种自动抓取logcat日志的能力。
logcat的小知识点
设备的logcat日志是有一个最大的缓存限制的,默认是256k,可以通过开发者选项去调整这个大小,也可以通过logcat -G 10M 设置。
设备重启后,logcat的缓存会被清空。
在终端中使用adb logcat命令获取log和在程序中调用logcat的命令获取log是两回事,获取到的日志信息往往是不同的,往往是在终端中获取的日志更全一些。
自定义ROM修改默认的logcat缓存大小
// kernel\drivers\staging\android\logger.c
// 在create_log方法的第二个参数传入自动定义的大小就好
static int __init
logger_init(void) {
int ret;
ret = create_log(LOGGER_LOG_MAIN, 256 * 1024 );
if (unlikely(ret))
goto out;
ret = create_log(LOGGER_LOG_EVENTS, 256 * 1024);
if (unlikely(ret))
goto out;
ret = create_log(LOGGER_LOG_RADIO, 256 * 1024 );
if (unlikely(ret))
goto out;
ret = create_log(LOGGER_LOG_SYSTEM, 256 * 1024);
if (unlikely(ret))
goto out;
out:
return ret;
}
在程序中获取log需要加android.permission.READ_LOGS权限。
android 4.1之前版本通过申请READ_LOGS权限就可以读取其他应用的log了。但是谷歌发现这样存在安全风险,于是android 4.1以及之后版本,即使申请了READ_LOGS权限也无法读取其他应用的日志信息了。4.1版本中 Logcat的签名变为 “signature|system|development”了,这意味着只有系统签名的app或者root权限的app才能使用该权限。普通用户可以通过ADB查看所有日志。
手机root后很多情况下也是无法在程序中使用root权限的,因为adb shell 的进程和程序执行的进程不是同一个,因此无法使用su命令。
抓取logcat的方式
- 方式一:读取日志流自己写入到文件中
private static boolean catchLogcatLog(String crashLogCatPath) {
if(TextUtils.isEmpty(crashLogCatPath)){
return false;
}
java.lang.Process logcatProcess = null;
BufferedReader bufferedReader = null;
BufferedOutputStream bufferedOutputStream = null;
try {
/** 获取系统logcat日志信息 */
String[] running = new String[]{"logcat"};
logcatProcess = Runtime.getRuntime().exec(running);
bufferedReader = new BufferedReader(new InputStreamReader(
logcatProcess.getInputStream()));
File file = new File(crashLogCatPath);
File parentFile = file.getParentFile();
if (!parentFile.exists() || !parentFile.isDirectory()) {
parentFile.mkdirs();
}
bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file, true));
String line;
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MM-dd HH:mm:ss");
String format = simpleDateFormat.format(new Date());
byte[] lineBytes = "\n".getBytes("UTF-8");
long s = SystemClock.elapsedRealtime();
// 10s
int maxWriteTime = 10000;
while ((line = bufferedReader.readLine()) != null) {
bufferedOutputStream.write(line.getBytes("UTF-8"));
bufferedOutputStream.write(lineBytes);
// 如果logcat打印到当前放生错误的位置或者打印时间大于10s就退出
if (line.contains(format) || Math.abs((SystemClock.elapsedRealtime() - s)) > maxWriteTime) {
// 跳出打印
break;
}
}
return true;
} catch (Exception e) {
Log.e("catch logcat error!");
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedOutputStream != null) {
try {
bufferedOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
- 方式二:完全使用shell命令写文件
private static boolean catchLogcatLogWithShell(String crashLogCatPath) {
if (TextUtils.isEmpty(crashLogCatPath)) {
return false;
}
ArrayList commnandList = new ArrayList();
//如果不删除之前的logcat.txt文件,每次执行logcat命令也不会更新该文件
commnandList.add("rm -r " + crashLogCatPath);
// 使用-d参数可以让logcat获取日志完毕后终止进程
// 如果使用">"是不会写入文件,必须使用-f的方式
commnandList.add("logcat -d -v time -f " + crashLogCatPath);
CommandResult commandResult = ShellUtils.execCommand(commnandList, false);
return commandResult.result == 0;
}
- 对比
方式一的实现逻辑更复杂,但是灵活性更好,可以在抓取的logcat文件中加入自定义信息,如在文件的开头写入设备的状态信息等。
方式二的实现比较简洁,完全交给系统处理文件的读写能比较好的保证日志的完整性。
大家可以根据自己的需求来选择实现方式。
工具类
- ShellUtils
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
/**
* ShellUtils
*
* Check root
* - {@link ShellUtils#checkRootPermission()}
*
*
* Execte command
* - {@link ShellUtils#execCommand(String, boolean)}
* - {@link ShellUtils#execCommand(String, boolean, boolean)}
* - {@link ShellUtils#execCommand(List, boolean)}
* - {@link ShellUtils#execCommand(List, boolean, boolean)}
* - {@link ShellUtils#execCommand(String[], boolean)}
* - {@link ShellUtils#execCommand(String[], boolean, boolean)}
*
*
*/
public class ShellUtils {
public static final String COMMAND_SU = "su";
public static final String COMMAND_SH = "sh";
public static final String COMMAND_EXIT = "exit\n";
public static final String COMMAND_LINE_END = "\n";
/**
* check whether has root permission
*
* @return
*/
public static boolean checkRootPermission() {
return execCommand("echo root", true, false).result == 0;
}
/**
* execute shell command
*
* @param command command
* @param isRoot whether need to run with root
* @param isNeedResultMsg whether need result msg
*
* @return
*
* @see ShellUtils#execCommand(String[], boolean, boolean)
*/
public static CommandResult execCommand(String command, boolean isRoot, boolean isNeedResultMsg) {
return execCommand(new String[]{command}, isRoot, isNeedResultMsg);
}
/**
* execute shell commands
*
* @param commands command array
* @param isRoot whether need to run with root
* @param isNeedResultMsg whether need result msg
*
* @return
* - if isNeedResultMsg is false, {@link CommandResult#successMsg} is null and
* {@link CommandResult#errorMsg} is null.
* - if {@link CommandResult#result} is -1, there maybe some excepiton.
*
*/
public static CommandResult execCommand(String[] commands, boolean isRoot, boolean isNeedResultMsg) {
int result = -1;
if (commands == null || commands.length == 0) {
return new CommandResult(result, null, null);
}
Process process = null;
BufferedReader successResult = null;
BufferedReader errorResult = null;
StringBuilder successMsg = null;
StringBuilder errorMsg = null;
DataOutputStream os = null;
try {
process = Runtime.getRuntime().exec(isRoot ? COMMAND_SU : COMMAND_SH);
os = new DataOutputStream(process.getOutputStream());
for (String command : commands) {
if (command == null) {
continue;
}
// donnot use os.writeBytes(commmand), avoid chinese charset error
os.write(command.getBytes());
os.writeBytes(COMMAND_LINE_END);
os.flush();
}
os.writeBytes(COMMAND_EXIT);
os.flush();
result = process.waitFor();
// get command result
if (isNeedResultMsg) {
successMsg = new StringBuilder();
errorMsg = new StringBuilder();
successResult = new BufferedReader(new InputStreamReader(process.getInputStream()));
errorResult = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String s;
while ((s = successResult.readLine()) != null) {
successMsg.append(s);
}
while ((s = errorResult.readLine()) != null) {
errorMsg.append(s);
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (os != null) {
os.close();
}
if (successResult != null) {
successResult.close();
}
if (errorResult != null) {
errorResult.close();
}
} catch (IOException e) {
e.printStackTrace();
}
if (process != null) {
process.destroy();
}
}
return new CommandResult(result, successMsg == null ? null : successMsg.toString(), errorMsg == null ? null
: errorMsg.toString());
}
/**
* execute shell command, default return result msg
*
* @param command command
* @param isRoot whether need to run with root
*
* @return
*
* @see ShellUtils#execCommand(String[], boolean, boolean)
*/
public static CommandResult execCommand(String command, boolean isRoot) {
return execCommand(new String[]{command}, isRoot, true);
}
/**
* execute shell commands, default return result msg
*
* @param commands command list
* @param isRoot whether need to run with root
*
* @return
*
* @see ShellUtils#execCommand(String[], boolean, boolean)
*/
public static CommandResult execCommand(List commands, boolean isRoot) {
return execCommand(commands == null ? null : commands.toArray(new String[]{}), isRoot, true);
}
/**
* execute shell commands, default return result msg
*
* @param commands command array
* @param isRoot whether need to run with root
*
* @return
*
* @see ShellUtils#execCommand(String[], boolean, boolean)
*/
public static CommandResult execCommand(String[] commands, boolean isRoot) {
return execCommand(commands, isRoot, true);
}
/**
* execute shell commands
*
* @param commands command list
* @param isRoot whether need to run with root
* @param isNeedResultMsg whether need result msg
*
* @return
*
* @see ShellUtils#execCommand(String[], boolean, boolean)
*/
public static CommandResult execCommand(List commands, boolean isRoot, boolean isNeedResultMsg) {
return execCommand(commands == null ? null : commands.toArray(new String[]{}), isRoot, isNeedResultMsg);
}
/**
* result of command
*
* - {@link CommandResult#result} means result of command, 0 means normal, else means error, same to excute in
* linux shell
* - {@link CommandResult#successMsg} means success message of command result
* - {@link CommandResult#errorMsg} means error message of command result
*
*
* @author Trinea 2013-5-16
*/
public static class CommandResult {
/**
* result of command
**/
public int result;
/**
* success message of command result
**/
public String successMsg;
/**
* error message of command result
**/
public String errorMsg;
public CommandResult(int result) {
this.result = result;
}
public CommandResult(int result, String successMsg, String errorMsg) {
this.result = result;
this.successMsg = successMsg;
this.errorMsg = errorMsg;
}
}
}