Android音视频开发之 自定义一个完备的log模块

  • 前言
    • 基础知识的掌握
      • Log系统
  • 为什么需要自定义一个log模块呢?
  • 做什么?
  • 怎么做?
      • 确定成员变量:
      • 初始化LogUtil
    • 输出功能的实现
    • write方法
      • 创建log/txt文件并且初始化IO流:
      • 自动清理七天产生的log
      • 压缩
      • 上传:
  • 总结:

前言

目前我自己的工作方向是基于Andriod适配层的音视频开发,那有关这个系列的博客都是我在实际的工作中遇到的一些问题和逐渐学习的过程,并且我也将会一直持续更新下去。

基础知识的掌握

作为一个音视频开发方向的程序员,无论基于何种OS(Android,IOS,Mac,Window,etc…)都需要去了解和音视频相关的基础知识,例如音频视频的编解码方式,主流编解码器的实现原理,音视频相关的参数等等许多。对于我而言我更倾向于遇到问题之后再去仔细的学习,毕竟在开始之前,谁都不知道自己会遇到什么样的问题,也不能真切的体会到自己在某些方面的不足。

Log系统

Log系统是我在工作中遇到的第一个任务,在我看来无论是何种应用任何方向,一个完善的Log模块都十分的重要。

为什么需要自定义一个log模块呢?

你好!作为一个Android应用的研发者,在搞代码的时候经常会使用到Log.d(w,e,i,v …) 等的语句辅助我们搞开发。无论是判断数据流向,程序执行顺序还是出现问题的时候通过logcat里面的信息对功能模块进行调整。如果仅仅是自己闷头自闭开发,使用系统的log和看AS的logcat的确足够了。但是要记住我们是一个研发者,开发的app是要给用户使用的。

  1. 如果我们开发并且测试好了,觉得这个app没有问题可以上线给用户用了,那么你是选择保留代码中的Log呢还是选择一个一个的把Log删掉呢。说句实在的如果你保留了log,用户用adb抓一下就看的一清二楚,个人觉得这样不是很好~反正我是不喜欢让别人看到我的log。并且log信息虽小,但是同样也是数据呀,如果执行的过多产生的增量会让程序包的体积变得越来越大。那我们只能一行一行的删除了嘛?当然不⬇️
  2. 当用户使用你的程序出现了错误,但是在你测试的时候却好好的没啥问题。那我们能够做的只有买一个用户的同款手机进行同样的测试,并且还要和用户做同样的操作。我个人不认为会有许多用户给程序员反馈信息,在程序崩溃的时候大多用户只会说一句:***,辣鸡,然后卸载;假设存在用户给你反馈信息,如果他不能准确的描述进行操作的步骤,那对于我们而言就真的麻烦死了。那如果我们能拿到当时写在程序里面的log信息。那排查起问题来就会变的简单一些。
  3. 保留一定时间下的log同样是个好处,便于上传log信息。

做什么?

  1. 给log设置显示与否的开关:平时的开发啊,或者beta版本可以正常的显示,方便调整问题。但在release版本可以直接设置为不可见(easy),但记住不可见≠不输出,并且可以很好的保护底层结构的不可见性。
  2. 给log设置是否输出到文件的开关:这个的意义在于由于Android的机型很多,在编码自测或者是提交给测试组测试的时候可能没有办法覆盖到所有机型,但是如果产品的客户或者用户很多,那我们不能保证所有的机型都不会出现问题。假设这种用户的手机出现了问题,我们查找问题修复问题的关键就在于能否找到这台机型并复现事故现场;但是如果我们有用户使用程序时崩溃前后的日志呢,那么也就省略了中间繁琐的过程,直接拿到日志信息去debug,这样的效率也会更高。
  3. 记录异常信息:所谓异常也就是在进程执行的过程中(很大程度上)由于代码结构的错误,底层执行顺序的错误所出现的,导致进程阻塞崩溃的Exception,常见的NullPointer,RunTime,IndexOutBond等等,如果用户出现了这样的Exception,那进程一定会崩溃。这些异常在代码中实际上很好去解决,如果为了防止崩溃,在前面加上一定的not null 或者防止越界的判断即可;如果为了真正彻底解决问题,还是要调整一下逻辑。在实际的应用中,由于用户不同的网络状况,延时,进程的并发,我们觉得一定不为null的对象也有可能为null,这种情况下不做处理就会给用户极差的崩溃体验。Log负责记录异常信息并收集使栈崩溃的原因。
  4. 自动上传 : 能够实现上传至服务器的功能,这样也方便开发者进行日志的提取。
    5.自动清理 :日志的数量不应该过多,防止占用用户过多的存储空间,只需要能确保记录下出现崩溃情况的日志即可。

怎么做?

确定成员变量:

public  class LogUtil {// 默认当前日志优先级为 2 也就是Verbose级private static int currentLevel=2;// 默认日志内容输出到logcatprivate static boolean out2logcat = true;// 默认写入本地文件.private static boolean is2Write = false;//用于执行IO操作的线程池 推荐使用缓存线程池。private static ThreadPoolExecutor mExecutor = null;// 日志文件的输出路径    eg : /storge/ andoird/emulator/0/data/packageName/fileprivate static String absFile = null;// 单个日志文件的名字private static String logFileName = null;// 标记LogUitil是否进行过初始化private static boolean initFlag = false;// IOprivate 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;elsecurrentLevel = 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();}//判断是否输出到logcatif(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() {@Overridepublic 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() {@Overridepublic 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很好用~

总结:

那这篇分享就到此结束了。

Android流媒体开发之 自定义一个完备的log模块相关推荐

  1. Android流媒体开发之-直播自定义分类

    1.Android流媒体开发之-直播实现 2.Android流媒体开发之-直播自定义列表 3.Android流媒体开发之-服务器图片的加载 4.Android流媒体开发之-直播自定义分类 5.Andr ...

  2. Android流媒体开发之-获取直播节目预告

    1.Android流媒体开发之-直播实现 2.Android流媒体开发之-直播自定义列表 3.Android流媒体开发之-服务器图片的加载 4.Android流媒体开发之-直播自定义分类 5.Andr ...

  3. android studio开发工具遇到一个新问题一直卡indexing paused due to batch update不停的转

    3.2.1以上版本的android studio开发工具遇到一个新问题一直卡indexing paused due to batch update不停的转 解决方法有一下两种方式: 1.第一种解决方式 ...

  4. 【Android 应用开发】 自定义组件 宽高适配方法, 手势监听器操作组件, 回调接口维护策略, 绘制方法分析 -- 基于 WheelView 组件分析自定义组件

    博客地址 : http://blog.csdn.net/shulianghan/article/details/41520569 代码下载 : -- GitHub : https://github.c ...

  5. Android实战开发:自定义照相机

    参考资料: SurfaceView:http://www.cnblogs.com/xuling/archive/2011/06/06/android.html android.hardware.Cam ...

  6. 基于Android输入法开发,制作一个微信斗图APP

    本文字数:5191字 预计阅读时间:20分钟 目录: 1 导读: 2 Android 输入法开发简介及流程: 3 斗图 APP 开发介绍: 4 斗图 APP 功能优化: 5 总结. 01 导读 微信斗 ...

  7. 【Android 应用开发】自定义View 和 ViewGroup

    一. 自定义View介绍 自定义View时, 继承View基类, 并实现其中的一些方法. (1) ~ (2) 方法与构造相关 (3) ~ (5) 方法与组件大小位置相关 (6) ~ (9) 方法与触摸 ...

  8. 【Android组件开发】自定义的多宫格布局

    FlexGridLayout 自定义的多宫格布局 目的: 之前有个需求需要用到仿IOS相册展示图片的布局方式,将图片以不同大小布局在一起,不像其他相册应用都是使用对称列表来展示图片,没有给人惊喜乃至于 ...

  9. Android Studio开发环境及第一个项目

    1. 在你的电脑上搭建Android平台开发环境. 2. 新建项目,实现以下基本内容: (1) 修改默认的APP的名称和图标(任意的,非默认的). (2) 显示个人信息,包括:照片.专业.姓名.学号等 ...

最新文章

  1. 安卓constraintLayout中app:srcCompat设置的图片显示不出来
  2. JAVA RPC:从上手到爱不释手
  3. Ubuntu下安装node canvas
  4. 【Groovy】字符串 ( 字符串注入函数 | asBoolean | execute | minus )
  5. 一:包装好和吹出去 二:三国心得
  6. linux的基础知识——信号的四要素和kill
  7. Netty工作笔记0049---阶段内容梳理
  8. windows下mysql备份脚本
  9. redis php教程pdf,ThinkPHP中简单使用Redis
  10. AT89C51的矩阵键盘、跑马灯和呼吸灯设计
  11. 硬盘容量的计算方法,这就是为什么实际容量总比官方标示少的原因
  12. 霍尔 磁电 光电式测数传感器的优缺点比较
  13. 我所热爱的多触摸系统 bill buxton
  14. docker学习1--docker基础学习
  15. 片上总线学习之Wishbone
  16. SAP SD初阶之VL10A为销售订单创建外向交货单
  17. 第八届山东省赛题 I Parity check 【找规律】
  18. 通通锁接口调用<Response [400]>报错及python示例代码
  19. 30 个案例教你用纯 CSS 实现常见的几何图形
  20. 前端访问nginx发布的视频文件,实现在线播放

热门文章

  1. 地址栏中输入网址后发生了什么?
  2. ∫e^(-x^2)dx怎么求 ??用的是什么方法??
  3. 爬虫框架Scrapy(西瓜皮)
  4. 计算机系统i3和i6区别,英特尔内核迭代,有i3 i5 i7,没有i4 i6吗?
  5. 7-FreeRTOS软件定时器
  6. 求大神帮忙解答!!!急!!!
  7. 强大的PubMed插件Scholarscope
  8. 让win7笔记本变成热点
  9. Daniel Jeffries:为什么我相信EOS是去中心化时代的黎明
  10. 软件测试行业中ta表示什么意思,温度冲击测试ta/tc分别代表什么意思