Android音视频开发之 自定义一个完备的log模块
前言
目前我自己的工作方向是基于Andriod适配层的音视频开发,那有关这个系列的博客都是我在实际的工作中遇到的一些问题和逐渐学习的过程,并且我也将会一直持续更新下去。
基础知识的掌握
作为一个音视频开发方向的程序员,无论基于何种OS(Android,IOS,Mac,Window,etc…)都需要去了解和音视频相关的基础知识,例如音频视频的编解码方式,主流编解码器的实现原理,音视频相关的参数等等许多。对于我而言我更倾向于遇到问题之后再去仔细的学习,毕竟在开始之前,谁都不知道自己会遇到什么样的问题,也不能真切的体会到自己在某些方面的不足。
Log系统
Log系统是我在工作中遇到的第一个任务,在我看来无论是何种应用任何方向,一个完善的Log模块都十分的重要。
为什么需要自定义一个log模块呢?
你好!作为一个Android应用的研发者,在搞代码的时候经常会使用到Log.d(w,e,i,v …) 等的语句辅助我们搞开发。无论是判断数据流向,程序执行顺序还是出现问题的时候通过logcat里面的信息对功能模块进行调整。如果仅仅是自己闷头自闭开发,使用系统的log和看AS的logcat的确足够了。但是要记住我们是一个研发者,开发的app是要给用户使用的。
- 如果我们开发并且测试好了,觉得这个app没有问题可以上线给用户用了,那么你是选择保留代码中的Log呢还是选择一个一个的把Log删掉呢。说句实在的如果你保留了log,用户用adb抓一下就看的一清二楚,个人觉得这样不是很好~反正我是不喜欢让别人看到我的log。并且log信息虽小,但是同样也是数据呀,如果执行的过多产生的增量会让程序包的体积变得越来越大。那我们只能一行一行的删除了嘛?当然不⬇️
- 当用户使用你的程序出现了错误,但是在你测试的时候却好好的没啥问题。那我们能够做的只有买一个用户的同款手机进行同样的测试,并且还要和用户做同样的操作。我个人不认为会有许多用户给程序员反馈信息,在程序崩溃的时候大多用户只会说一句:***,辣鸡,然后卸载;假设存在用户给你反馈信息,如果他不能准确的描述进行操作的步骤,那对于我们而言就真的麻烦死了。那如果我们能拿到当时写在程序里面的log信息。那排查起问题来就会变的简单一些。
- 保留一定时间下的log同样是个好处,便于上传log信息。
做什么?
- 给log设置显示与否的开关:平时的开发啊,或者beta版本可以正常的显示,方便调整问题。但在release版本可以直接设置为不可见(easy),但记住不可见≠不输出,并且可以很好的保护底层结构的不可见性。
- 给log设置是否输出到文件的开关:这个的意义在于由于Android的机型很多,在编码自测或者是提交给测试组测试的时候可能没有办法覆盖到所有机型,但是如果产品的客户或者用户很多,那我们不能保证所有的机型都不会出现问题。假设这种用户的手机出现了问题,我们查找问题修复问题的关键就在于能否找到这台机型并复现事故现场;但是如果我们有用户使用程序时崩溃前后的日志呢,那么也就省略了中间繁琐的过程,直接拿到日志信息去debug,这样的效率也会更高。
- 记录异常信息:所谓异常也就是在进程执行的过程中(很大程度上)由于代码结构的错误,底层执行顺序的错误所出现的,导致进程阻塞崩溃的Exception,常见的NullPointer,RunTime,IndexOutBond等等,如果用户出现了这样的Exception,那进程一定会崩溃。这些异常在代码中实际上很好去解决,如果为了防止崩溃,在前面加上一定的not null 或者防止越界的判断即可;如果为了真正彻底解决问题,还是要调整一下逻辑。在实际的应用中,由于用户不同的网络状况,延时,进程的并发,我们觉得一定不为null的对象也有可能为null,这种情况下不做处理就会给用户极差的崩溃体验。Log负责记录异常信息并收集使栈崩溃的原因。
- 自动上传 : 能够实现上传至服务器的功能,这样也方便开发者进行日志的提取。
5.自动清理 :日志的数量不应该过多,防止占用用户过多的存储空间,只需要能确保记录下出现崩溃情况的日志即可。
怎么做?
确定成员变量:
public class LogUtil {
// 默认当前日志优先级为 2 也就是Verbose级
private static int currentLevel=2;
// 默认日志内容输出到logcat
private static boolean out2logcat = true;
// 默认写入本地文件.
private static boolean is2Write = false;
//用于执行IO操作的线程池 推荐使用缓存线程池。
private static ThreadPoolExecutor mExecutor = null;
// 日志文件的输出路径 eg : /storge/ andoird/emulator/0/data/packageName/file
private static String absFile = null;
// 单个日志文件的名字
private static String logFileName = null;
// 标记LogUitil是否进行过初始化
private static boolean initFlag = false;
// IO
private static FileOutputStream fos = null;
private static OutputStreamWriter osWritter= null;
private static BufferedWriter bw = null;
// 压缩文件的绝对路径
private static String sDestinationPath;
// 压缩文件的名字
private static String sZipFilePathName;
private static Throwable IOException;
private static String sZip2FatherFilePath;
// 用于格式化日期的工具
private static SimpleDateFormat sSimpleDateFormat = null;
private static SimpleDateFormat sSp = null;
}
那这些都比较好理解,会在下面功能完善的过程中逐渐使用到。写到这里的目的是下面我就不想写注释了= =。
初始化LogUtil
/**
* initialize util.
* @param context 2 get the abs path
* @param level 2 control view of log
* @param out2logcat true means output log to logcat
* @param isWrite2File write 2 file or not
*/
public static void intializeLogUtil(Context context, int level, boolean out2logcat, boolean isWrite2File) {
//初始化线程池
if (mExecutor == null) {
mExecutor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
}
//标记log已经被初始化
initFlag = true;
//初始化日期工具类
if (sSp == null) {
sSp = new SimpleDateFormat("yyyyMMdd-HHmmss");
}
if (sSimpleDateFormat == null) {
sSimpleDateFormat = new SimpleDateFormat("MM:dd HH:mm:ss.SSS");
}
//创建日志文件
File externalFilesDir = context.getExternalFilesDir(null);
if (externalFilesDir!=null&&externalFilesDir.exists()) {
absFile = externalFilesDir.getPath();
}else{
absFile = Environment.getExternalStorageDirectory().getPath();
}
//初始化日志优先级
if(level>8)
currentLevel = 8;
else if(level<2)
currentLevel = 2;
else
currentLevel = level;
//初始化是否输出到日志和是否输出到文件
LogUtil.out2logcat = out2logcat;
is2Write = isWrite2File;
// 初始化zip文件的目录
if (sZip2FatherFilePath == null) {
sZip2FatherFilePath = absFile+"/lbyLogZip";
}
//初始化日志文件的目录
if (sLogFileDirName == null) {
sLogFileDirName = absFile+"/lbyLogFile";
}
// 创建文件
File file1 = new File(sLogFileDirName);
File file = new File(sZip2FatherFilePath);
if (!file.exists()) {
file.mkdir();
}
if (!file1.exists()) {
file1.mkdir();
}
}
这里留给外部调用的初始化接口,通过传入的各种参数初始化log工具,传入的context用于获取创建log文件存放的目录,创建zip文件传入的目录。
输出功能的实现
为了要像原来用Log.d等的语句一样,用起来简单方便,还要实现功能的完善,所以可能需要重载很多方法,比较好理解 因为d w i 等等的实现都是一样的,所以这里只贴出一种实现的方式
/**
* handle exception.
* @param tag
* @param msg
* @param is2Write whether 2 wirte
* @param throwable System exception, we care about exception much more that other's.
*/
public static void i(Object tag, String msg,boolean is2Write,Exception throwable){
if (currentLevel > LogLevel.INFO.val)
return;
String strTAG ;
//判断传入的tag的类型,如果不是string或者class,默认获取tag 传入的class的名字
if(tag instanceof String){
strTAG = tag.toString();
}else if(tag instanceof Class) {
strTAG = ((Class) tag).getSimpleName();
}else{
strTAG = tag.getClass().getName();
}
//判断是否输出到logcat
if(out2logcat){
Log.i(strTAG,msg);
}
//判断是否输出到文件
if(is2Write){
//write 方法。
write(strTAG,msg,LogLevel.INFO.val,throwable);
}
}
/**
* use default write config
* @param tag
* @param msg
*/
public static void i(Object tag, String msg){
//如果未传入是否写文件,则使用默认的配置
i(tag,msg,is2Write);
}
/**
* depend on the currentLevel .
*
* @param tag{Object} use class name or simple string.
* @param msg{String} log msg.
* @param is2Write whether 2 wirte
*/
//如果未传入异常,则认为异常为空
public static void i(Object tag, String msg,boolean is2Write){
i(tag,msg,is2Write,null);
}
//如果未传入写文件,则使用默认配置
public static void i(Object tag, String msg,Exception throwable){
i(tag,msg,iswWrite,throwable);
}
(d w e v等同理)可以看到目前重载了4个方法,也就是说可以通过4个不同的入口进行Log的输出,也保留的默认的入口方便使用,默认的入口使用在 初始化 阶段已经进行了配置,传入 异常 的情况是在进行try catch的时候传入异常进行栈打印。保证调用log模块的简单和人性化
write方法
/**
* when d v i e w etc . are invoked, LogUtil will write the log
* info 2 local file(after setAbsPath()are invoked),
* if set() haven't called , and it is needed to write the log
* info into file, it will write to timeStamp_LogBeforeSetAbsPath.txt;
* intializeLog() offer a default params to be the whole controler
* @param tag just tag
* @param msg log info
* @param level log priority
* @param expectation expectioin.
*/
private static void write(String tag,String msg,int level,Exception expectation){
// 如果log被在 初始化 之前调用, (注意这里是文件 不是目录,在初始化阶段只
// 设置了父目录,并未创建单个log文件)
if (logFileName==null||logFileName.isEmpty()) {
if(sSp!=null) {
logFileName = sSp.format(new Date(System.currentTimeMillis())) + "_LogBeforeSetAbsPath.txt";
File file = new File(sLogFileDirName + "/" + logFileName);
if (!file.exists()) {
try {
file.createNewFile();
fos = new FileOutputStream(file);
} catch (java.io.IOException e) {
e.printStackTrace();
}
osWritter = new OutputStreamWriter(fos);
bw = new BufferedWriter(osWritter);
}
}
}
if(bw == null) {
return;
}
String now = sSimpleDateFormat.format(new Date(System.currentTimeMillis()));
try {
//输出log到文件的log的格式
bw.write(now+ " "+ Thread.currentThread().getName()+" "+level+"/"+tag+":");
bw.write(msg);
bw.newLine();
bw.flush();
osWritter.flush();
fos.flush();
} catch (IOException e) {
e.printStackTrace();
}
if(expectation!=null){
try {
solveTheException(expectation);
} catch (IOException e) {
e.printStackTrace();
}
}
}
_write()_方法实现打印msg到文件内。并且为了防止单个log文件还没有被创建而造成的异常,write会自行判断文件是否存在,如果不存在那就先创建再写入。
创建log/txt文件并且初始化IO流:
/**
* set absPath , used in Engine.joinRoom(), when user join a room, create a new LogFile and initWritter.
* and we can also create a new File in these method.
* @param unixStamp not absPath , just file name. eg:"123123(format time)_roomID_uid.text"
* ...
*/
public static boolean setAbsPath(Long unixStamp,String roomID,String userID){
if(!initFlag) {
return false;
}
fos =null;
osWritter =null;
bw = null;
String absPath1 = sSp.format(new Date(unixStamp))+"_"+roomID+"_"+userID+".txt";
if(absPath1!=null)
{
logFileName = absPath1;
File file = new File(sLogFileDirName+"/"+logFileName);
if(!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
//initialized the writters ,
try {
fos = new FileOutputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
osWritter = new OutputStreamWriter(fos);
bw = new BufferedWriter(osWritter);
return true;
}
return false;
}
这里可能会有人问为什么不再创建父目录的时候直接将单个文件创建出来,emm,因产品而异,毕竟创建文件的时候可能会需要一些参数, 但是这些参数在调用init 方法的时候并没有,所以说设置了,但是初始化方法需要的参数 也就是context ,可能在创建单个文件的时候没有,所以分为两步。如果对文件名没有过多的要求的话, 自然也可以合二为一。不过文件名对于我来说其实挺重要的,因为log文件可能会很多,也就是说一个用户在登陆应用直到退出应用的时候虽然只产生一个,但是如果用户一天5次进入应用呢就会产生5个文件,如果用户的量有10w个呢,就会有太多太多的log文件需要去找,打包的目的也是为了能够快速的定位到问题用户的zip文件(里面装的全都是用户几天内使用应用的产生的log),并且还要在多个log中找到用户出现问题那一次所产生的log,那如果不对文件名进行规范,或者容易查找的话,简直大海捞针,比debug还痛苦。
所以我这里的策略就是规范命名规则 使用timeStamp_deviceModule (20180909_1111_RedMiNote3),这样如果用户告诉我大概在哪一天,使用的是什么手机,我就可以迅速的在许许多多个文件中找到问题用户的文件,找到出现问题的日志信息然后解决问题。
自动清理七天产生的log
这个方法在初始化log之后进行调用,防止用户产生过多的无用的log。
/**
* auto clean the log files which are more than 7 days.
*/
public static void cleanLogFile(){
File file = new File(sLogFileDirName);
//获取今天的时间
long today = System.currentTimeMillis()/(1000*60*60*24);
//这里注意一下 f这里使用的file 必须是一个目录级 文件, 所以增加一个判断
String[] children = file.list();
// 判断获取到的文件名列表是不是空。
if(children == null) return;
mExecutor.execute(new Runnable() {
@Override
public void run() {
for(String childrenFileName:children){
String[] s = childrenFileName.split("_");
try {
//由于我的命名规则是 日期_设备号 所以我获取第一位日期
//计算是否大于七天再考虑进行删除
long formal = sSp.parse(s[0]).getTime()/(1000*60*60*24);
if(Math.abs(today-formal)>=7)
File file1 = new File(sLogFileDirName+"/"+childrenFileName);
if (file1.delete()) {
Log.d("lbTest","delete success"+file1.getName());
}else{
Log.d("lbTest","delete fail"+file1.getName());
}
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
});
}
压缩
/**
* @param observer async method , when zip is finished, observer.onLogFileReady()'ll be called.
* @return if LogUtil haven't been initialized, it will return false, needed be init before use.
*/
public static boolean zipLogFile(LivePlayerObserver observer){
// needed initialize.
if(!initFlag) {
return false;
}
SimpleDateFormat sp = new SimpleDateFormat("yyyyMMdd-HHmmss");
String s = Build.MODEL.replaceAll(" ", "");
sZipFilePathName = sp.format(new Date(System.currentTimeMillis())).toString()+"_"+s+".zip";
sDestinationPath = sZip2FatherFilePath+"/"+sZipFilePathName;
LBYzip(sDestinationPath,observer);
return true;
}
private static void LBYzip(String destinationPath, LivePlayerObserver observer) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
try {
zip(sLogFileDirName,destinationPath);
String[] zipFileList = getZipFileList();
if(zipFileList!=null&&zipFileList.length!=0){
observer.onLogFileReady(zipFileList);
}else{
observer.error(Errors.E40002);
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
public static String[] getZipFileList(){
File file = new File(sZip2FatherFilePath);
if(file.exists()) {
return file.list();
}
return null;
}
/**
* @param src source file abs path
* @param dest destnation file path .
* @throws IOException
*/
private static void zip(String src, String dest) throws IOException {
File tempT = new File(src);
if(tempT.exists()){
tempT.delete();
}
ZipOutputStream out = null;
try {
File outFile = new File(dest);
File fileOrDirectory = new File(src);
out = new ZipOutputStream(new FileOutputStream(outFile));
if (fileOrDirectory.isFile()) {
zipFileOrDirectory(out, fileOrDirectory, "");
} else {
File[] entries = fileOrDirectory.listFiles();
for (int i = 0; i < entries.length; i++) {
zipFileOrDirectory(out, entries[i], "");
}
}
} catch (IOException ex) {
ex.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
提供zip的输出路径,提供一个oberver告诉外面的调用者log压缩的成功或者失败,如果成功就提供一个压缩好的路径;如果失败就通过回调告诉给外边的失败处理机制。
上传:
对于先前说的上传功能,由于不同的服务器接收的post原则不同,这里就不贴出自己的代码了,毕竟okhttp很好用~
总结:
那这篇分享就到此结束了。