概述
最近开发写一个Log的追踪日志。
将log信息写入到cache文件下的文件中,当遇到Crash的Log或者Log文件大小大于1mb,则上传至服务器。
那么首先,我们知道,上传服务器的操作一般要开子线程,而多个子线程同时写Log,那肯定就要考虑使用单例模式了:
public class LogTool {
private static final String TAG = "LogTool";
private static LogTool instance;
private static String apiUrl = "http://xxxx";
private LogTool() {
}
public static LogTool getInstance() {
if (instance == null)
instance = new LogTool();
return instance;
}
}
接下来我们要考虑log日志的存储位置,我这里是放在cache文件下的(本身文件也不大,也要多写),我们在这里写init()方法来初始化路径,这个方法要在application类调用,在整个生命周期开始时就要执行:
//初始化
public void init(Context context) {
//获得文件存储路径
logPath = getFilePath(context);
}
private static String getFilePath(Context context) {
if (Environment.MEDIA_MOUNTED.equals(Environment.MEDIA_MOUNTED) || !Environment.isExternalStorageRemovable()) {
//如果外部储存可用 则存储在外部的缓存文件中
return context.getExternalCacheDir().getPath();
} else {
//否则直接存在内部储存的缓存文件中
return context.getCacheDir().getPath();
}
}
然后我们根据需求来划分Log等级,一般都为 debug<info<warning<error<crash
并提供对外的打印方法。
注:这里的等级划分、和打印方法所要的信息都是跟着需求来。但是都是大同小异。我这里多写什么信息,打印方法也只留最基本的
/**
* Log的分级为 Crash、Error、Warning、Info、Debug
*/
public static final String CRASH_LEVEL = "CRASH";
public static final String ERROR_LEVEL = "ERROR";
public static final String WARNING_LEVEL = "WARNING";
public static final String INFO_LEVEL = "INFO";
public static final String DEBUG_LEVEL = "DEBUG";
public void c(String logData) {
writeToFile(CRASH_LEVEL, logData);
}
public void e(String logData) {
writeToFile(ERROR_LEVEL, logData);
}
public void w(String logData) {
writeToFile(WARNING_LEVEL, logData);
}
public void i(String logData) {
writeToFile(INFO_LEVEL, type, logData);
}
public void d(String logData) {
writeToFile(DEBUG_LEVEL, logData);
}
witeTofile就是写入到文件的方法:
private static StackTraceElement getCallerStackTraceElement() {
return Thread.currentThread().getStackTrace()[5];
}
/**
* 将Log信息写入文件
* isDouble为是否连续两次写入,防止连续两次上传服务器。
*
* @param level
* @param type
* @param logData
*/
public static void writeToFile(String level, String logData) {
if (null == logPath) {
LogUtil.e(TAG, "logPath == null ,未初始化LogToFile");
return;
}
String fileName = logPath + "/AppLogs_Android.log";
StackTraceElement caller = getCallerStackTraceElement();
// 获取到类名
String callerClazzName = caller.getClassName();
callerClazzName = callerClazzName.substring(callerClazzName
.lastIndexOf(".") + 1);
//要写入的LOG内容
String log = type + " - " + callerClazzName + " - " + caller.getMethodName() + " - line " + caller.getLineNumber() + " - " + logData + "n";
LogUtil.d(TAG, log);
//如果父路径不存在
File file = new File(logPath);
if (!file.exists()) {
file.mkdirs();//创建父路径
}
FileOutputStream fos = null;
BufferedWriter bw = null;
try {
fos = new FileOutputStream(fileName, true);
bw = new BufferedWriter(new OutputStreamWriter(fos));
bw.write(log);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bw != null) {
bw.close();//关闭缓冲流
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (getLogsFileSize(fileName) >= 1f) {
sendToServer(fileName);
} else if (level == CRASH_LEVEL || level == ERROR_LEVEL || level == WARNING_LEVEL) {
sendToServer(fileName);
}
}
上面有一个巨顶的方法:
Thread.currentThread().getStackTrace()[5]
这行函数能够得离当前执行代码的指令。(注:不一定是5)
因为在执行命令时,指令栈保存当前线程最近执行的代码。
它的原理是这样的:
1、MainActivity : LogTool.d(xxx,xxx)
跳转-》
2、LogTool: d(xxx,xx){wirteToFile(xxx,xxx)}
跳转-》
3、LogTool:writeToFile(xxx){}
假如上面是一个指令栈,那么我就去获取这个栈的栈底元素 [1] MainActivity… 这一行,得到的是个StackTraceElement
对象。
得到该对象之后,我们可以将该指令反射,通过getClassName()
获取类名,通过getMethodName()
获取方法名、通过getLineNumber()
获取当前行数,就省的我们一步一步去找具体哪行了。
所以上面代码中的第5行只是我这边的情况,别的代码就要自己去推理具体在哪一行咯。
接下来就是上传到服务器,这里使用Okhttp,上传的格式是包括文件+一些附带String信息,所以使用mutipart来提交表单,并且开启一个线程来提交。
/**
* 上传Log文件至服务器
*
* @return
*/
public static void sendToServer(final String pathName) {
new Thread(new Runnable() {
@Override
public void run() {
final File file = new File(pathName);
MediaType MEDIA_TYPE_TXT = MediaType.parse("text/plain");
RequestBody fileBody = MultipartBody.create(MEDIA_TYPE_TXT, file);
MultipartBody multiBuilder = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("log_file", file.getName(), fileBody)
.addFormDataPart("system", "Android")
.addFormDataPart("phone_number", "110").build();
Request request = new Request.Builder().url(apiUrl).post(multiBuilder).build();
OkHttpClient okHttpClient = new OkHttpClient();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//请求失败的处理
LogUtil.e(TAG, "上传Log文件失败 : " + e.toString());
if (getLogsFileSize(pathName) > 1f) {
file.delete();
}
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//请求成功的处理
LogUtil.d(TAG, "上传Log文件成功 : " + response.body().string());
//清空文件
file.delete();
}
});
}
}).start();
}
到这里就上传完啦。
但是我们还没有关注Crash的捕捉,因为我们自己无法判断Crash是出现在什么地方的,所以我们要写一个全局的Crash捕捉器,而Android提供了这个类,叫:Thread.UncaughtExceptionHandler,我们通过实现该接口,重写uncaughtException()方法,来处理遇到异常的情况。
该类如下(也需要在App初始化时init):
public class CrashHandlerManager implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandlerManager";
private static CrashHandlerManager instance;
private Context context;
private Thread.UncaughtExceptionHandler uncaughtExceptionHandler;
//收集信息集合
private Map<String, String> infoMap = new HashMap<>();
public synchronized static CrashHandlerManager getInstance() {
if (instance == null) {
instance = new CrashHandlerManager();
}
return instance;
}
private CrashHandlerManager() {
}
/**
* 初始化程序异常处理器
*
* @param context
*/
public void initCrash(Context context) {
this.context = context;
//获取系统默认的UncaughtException处理器
uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
//设置该CrashHandler为程序得默认处理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 当UncaughtException发生会进入此方法
*
* @param t
* @param e
*/
@Override
public void uncaughtException(Thread t, Throwable e) {
if (!handleException(e) && uncaughtExceptionHandler != null) {
//如果自定义没有处理就交给系统去处理
uncaughtExceptionHandler.uncaughtException(t, e);
}
}
private boolean handleException(Throwable e) {
if (e == null) {
return false;
}
collectionDeviceInfo(context, e.toString());
return true;
}
/**
* 收集错误处理信息
*
* @param context
* @param e
*/
private void collectionDeviceInfo(Context context, String e) {
//获得包管理器
try {
PackageManager pm = context.getPackageManager();
//获取该应用信息
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
e.printStackTrace(printWriter);
StringBuffer stringBuffer = stringWriter.getBuffer();
e.printStackTrace();
String versionName = pi.versionName == null ? "null" : pi.versionName;
String versionCode = pi.versionCode + "";
infoMap.put("versionName", versionName);
infoMap.put("versionCode", versionCode);
infoMap.put("phone_brand", Build.BRAND);
infoMap.put("phone_version", Build.VERSION.RELEASE);
infoMap.put("error", stringBuffer.toString());
}
} catch (PackageManager.NameNotFoundException e1) {
e1.printStackTrace();
LogUtil.e(TAG, "获取信息失败");
}
Field[] fields = Build.class.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
infoMap.put(field.getName(), field.get("").toString());
} catch (IllegalAccessException ex) {
ex.printStackTrace();
}
}
LogTool.getInstance().c(LogTool.CRASH_LOG, infoMap.toString());
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.exit(0);
}
}
在collectionDeviceInfo方法中,我们对遇到的错误的地方进行了收集和记录Log,并且在记录完时:
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.exit(0);
这里让主线程 sleep3秒的原因是 Crash会造成主线程的停止,而停止会导致上传文件的线程也会停止,这就会导致文件上传不了,所以我们让主线程sleep的期间让子线程赶紧去上传,3s的速度即使是3G网络也能上传好1M以下的文件了(除非真的很垃圾的网)
然后在通过exit来回到主界面去。
ok,自己封装的一个Log工具就讲到这里了,这个Log工具还是比较简单的,所以可能会有一些问题。
比如在不断记录log时,频繁的打开和关闭输出流其实会耗费一定的性能,但我又不能上传时再将Log信息输入到文件中(因为这样不是实时的,不能知道输入完后是不是文件早早超出了1M)。
在输出的时候 使用了 BufferWriter来优化输出效率,(其实也快不了多少)
至于别的 关于 OkHttp的上传方式、Thread.UncaughtExceptionHandler使用的正确姿势,网上资料也很多,这里就不再赘述了。
最后
以上就是谨慎热狗为你收集整理的Android 封装Log工具并上传Log文件到服务器(带类名、方法名、行数、Crash的捕捉)的全部内容,希望文章能够帮你解决Android 封装Log工具并上传Log文件到服务器(带类名、方法名、行数、Crash的捕捉)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复