Android 封装Log工具并上传Log文件到服务器(带类名方法名行数Crash的捕捉)
Posted RikkaTheWorld
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 封装Log工具并上传Log文件到服务器(带类名方法名行数Crash的捕捉)相关的知识,希望对你有一定的参考价值。
最近开发写一个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的捕捉)的主要内容,如果未能解决你的问题,请参考以下文章