Diycode开源项目 BaseApplication分析+LeakCanary第三方+CrashHandler自定义异常处理
1.BaseApplication整个应用的开始
1.1.看一下代码
/* * Copyright 2017 GcsSloop * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Last modified 2017-03-11 22:24:54 * * GitHub: https://github.com/GcsSloop * Website: http://www.gcssloop.com * Weibo: http://weibo.com/GcsSloop */ package com.gcssloop.diycode.base.app; import android.app.Application; import com.gcssloop.diycode.utils.Config; import com.gcssloop.diycode.utils.CrashHandler; import com.gcssloop.diycode_sdk.api.Diycode; import com.squareup.leakcanary.LeakCanary; public class BaseApplication extends Application { public static final String client_id = "7024a413"; public static final String client_secret = "8404fa33ae48d3014cfa89deaa674e4cbe6ec894a57dbef4e40d083dbbaa5cf4"; @Override public void onCreate() { super.onCreate(); if (LeakCanary.isInAnalyzerProcess(this)) { return; } LeakCanary.install(this); CrashHandler.getInstance().init(this); Diycode.init(this, client_id, client_secret); Config.init(this); } }
1.2.代码预览
首先是两个静态变量,是客户的的id和密钥。
然后是一个onCreate函数,进行一些必要的初始化。
1.3.然后调用Application的onCreate函数
这里用了一个第三方库LeakCanary。
首先install这个application
这里的CrashHandler的作用是崩溃处理办法。
然后就用将上面的客户的id和客户的密码来初始化了。
用户设置Config初始化也是这里进行。
2.LeakCanary内存泄露检测
2.1.首先了解一下什么是LeakCanary。
LeakCanary是检测APP内存泄露的工具,内存泄漏是Android开发中常用的问题,导致程序的稳定性下降。
2.2.在build.gradle中加入:
2.3.在Application中初始化
配置非常简单,会增加一个附属应用,去掉Application的引用,就可以移除这个附属应用了。
建议在开发模式不要去掉这个引用,在发布版本一定就要移除这个引用了。
这个开源项目的效果是这样的(这是开发模式)
2.4.注意在清单中定义这个Application
2.5.什么情况会发生内存泄露?(参考文章:使用LeakCanary检测内存泄露)
假设我们有一个MainActivity,它的布局很简单,里面只有一个TextView。
现在我们写一个单例xxxHelper之类的业务类==>用来给主活动中的TextView固定设一个值。但是这个值要从res
中读取,所以我们得用到Context。
现在我们回到MainActivity中来使用这个单例:
然后附属应用直接发出内存泄漏的提示。
为什么会发生内存泄漏呢?
==>LeakCanary已经把问题很明显地带到我们面前。这是一个典型的单例导致的Context泄漏问题。我们知道
Android的Context分为Activity Context和Application Context。
关于他们的区别,请参考一下这篇文章。如
果没时间看也没关系,其实一个返回的是当前Activity的实例,另一个是项目的Application的实例。Context的
应用场景参考一下下方的图片。
从程序的角度上来理解:Context是个抽象类,而Activity,Service,Application等都是该类的一个实现。
在上方那段简单的代码中,我们的xxxHelper的静态实例ourInstance由于有一个对mTextView的引用,而
mTextView由于要setText(),所以持有了一个对Context的引用,而我们在MainActivity里获取xxxHelper
实例时因为传入了MainActivity和Context,这使得一旦这个Activity不在了之后,xxxHelper依然会hold住
它的Context不放,而这个时候因为Activity已经不在了,所以内存泄漏自然就产生了。
尝试解决==>
这种写法治标不治本。尽管我们的Context已经是Application层级的Context了,但是这种写法依然会导致
mTextView在退出后依旧hold住整个Application的Context,最终还是导致内存泄漏。
正确解决方案==>
采用Application级别的Context+增加一个移除TextView的引用。在onDestroy调用移除函数即可。
3.CrashHandler异常处理
3.1.这个类是一个异常处理类。
有点类似腾讯的Bugly==>也是一个异常处理的一个强大第三方库。它可以知道每次APP崩溃的原因,以及提示
解决方案。之前的“大学喵”项目我就是用的Bugly,每次异常,它都会给我发送一封邮件,告诉我哪里崩溃,
哪里异常,每天早上也会统计今日崩溃次数等。非常简单实用的第三方库。
3.2.这里提供一下开源项目Diycode的异常处理类的源码。
/* * Copyright 2017 GcsSloop * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Last modified 2017-03-08 01:01:18 * * GitHub: https://github.com/GcsSloop * Website: http://www.gcssloop.com * Weibo: http://weibo.com/GcsSloop */ package com.gcssloop.diycode.utils; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Environment; import android.os.Process; import android.util.Log; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.Date; public class CrashHandler implements Thread.UncaughtExceptionHandler { private static final String TAG = "CrashHandler"; private static final boolean DEBUG = true; private static final String PATH = Environment.getExternalStorageDirectory().getPath() + "/ryg_test/log/"; private static final String FILE_NAME = "crash"; //log文件的后缀名 private static final String FILE_NAME_SUFFIX = ".trace"; private static CrashHandler sInstance = new CrashHandler(); //系统默认的异常处理(默认情况下,系统会终止当前的异常程序) private Thread.UncaughtExceptionHandler mDefaultCrashHandler; private Context mContext; //构造方法私有,防止外部构造多个实例,即采用单例模式 private CrashHandler() { } public static CrashHandler getInstance() { return sInstance; } //这里主要完成初始化工作 public void init(Context context) { //获取系统默认的异常处理器 mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler(); //将当前实例设为系统默认的异常处理器 Thread.setDefaultUncaughtExceptionHandler(this); //获取Context,方便内部使用 mContext = context.getApplicationContext(); } /** * 这个是最关键的函数,当程序中有未被捕获的异常,系统将会自动调用#uncaughtException方法 * thread为出现未捕获异常的线程,ex为未捕获的异常,有了这个ex,我们就可以得到异常信息。 */ @Override public void uncaughtException(Thread thread, Throwable ex) { try { //导出异常信息到SD卡中 dumpExceptionToSDCard(ex); //这里可以通过网络上传异常信息到服务器,便于开发人员分析日志从而解决bug uploadExceptionToServer(); } catch (IOException e) { e.printStackTrace(); } //打印出当前调用栈信息 ex.printStackTrace(); //如果系统提供了默认的异常处理器,则交给系统去结束我们的程序,否则就由我们自己结束自己 if (mDefaultCrashHandler != null) { mDefaultCrashHandler.uncaughtException(thread, ex); } else { Process.killProcess(Process.myPid()); } } private void dumpExceptionToSDCard(Throwable ex) throws IOException { //如果SD卡不存在或无法使用,则无法把异常信息写入SD卡 if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { if (DEBUG) { Log.w(TAG, "sdcard unmounted,skip dump exception"); return; } } File dir = new File(PATH); if (!dir.exists()) { dir.mkdirs(); } long current = System.currentTimeMillis(); String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(current)); //以当前时间创建log文件 File file = new File(PATH + FILE_NAME + time + FILE_NAME_SUFFIX); try { PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file))); //导出发生异常的时间 pw.println(time); //导出手机信息 dumpPhoneInfo(pw); pw.println(); //导出异常的调用栈信息 ex.printStackTrace(pw); pw.close(); } catch (Exception e) { Log.e(TAG, "dump crash info failed"); } } private void dumpPhoneInfo(PrintWriter pw) throws PackageManager.NameNotFoundException { //应用的版本名称和版本号 PackageManager pm = mContext.getPackageManager(); PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager .GET_ACTIVITIES); pw.print("App Version: "); pw.print(pi.versionName); pw.print('_'); pw.println(pi.versionCode); //android版本号 pw.print("OS Version: "); pw.print(Build.VERSION.RELEASE); pw.print("_"); pw.println(Build.VERSION.SDK_INT); //手机制造商 pw.print("Vendor: "); pw.println(Build.MANUFACTURER); //手机型号 pw.print("Model: "); pw.println(Build.MODEL); //cpu架构 pw.print("CPU ABI: "); pw.println(Build.CPU_ABI); } private void uploadExceptionToServer() { //TODO Upload Exception Message To Your Web Server } }
3.3.首先看一下成员变量。
首先是一个TAG,方便在logcat中进行输出。
然后是一个本项目是debug模式还是release模式,然后做出相应的改变。
然后是一个确定将异常信息输出到哪个地址,这里放在一个log中的。
然后输出的文件名为“crash”
然后确定log文件的后缀名为.trace
然后是系统默认的异常处理,默认情况下,系统会终止当前的异常程序。
当然,Context也是不能少的,这里用来获取真实的上下文的。
注意这里已经new了一个CrashHandler()了。
3.4.构造函数+单例模式
这里的单例模式,直接返回在成员变量中new的一个CrashHandler。
3.5.初始化工作==>在Application中调用
可以获取系统默认的异常处理器,然后可以将当前实例设为系统默认的异常处理器。
这里获得了一个全局的上下文。
3.6.关键Override的函数
异常信息调用函数dumpExceptionToSDCard来输入SD卡中。
还可以自己写一个函数uploadExceptionToServer上次到服务器中,方便调试。
然后打印出当前调用栈信息。
如果系统提供了默认的异常处理器,则交给系统去结束我们的程序,否则就由我们自己结束自己。
如何自己结束自己呢?
Process.killProcess(Process.myPid())==>有时候,关闭APP,也会采用这种极端的方法。
3.7.如何输入SD卡中?
首先判断SD卡是否存在而且是否可用,还要判断是否是DEBUG模式。
然后判断路径是否存在。
然后获取系统当前时间,文件名有3部分组成:路径+名称+后缀
然后调用系统的io类PrintWriter来导出时间,手机信息,调用栈新消息。
3.8.如何导出手机信息?
这里利用上下文,获得包管理器getPackageManager。
利用包管理器再得到包信息,pm.getPackageInfo
然后可以得到android版本号,手机制造商,手机型号,cpu架构。
4.Config自定义通用类-用户设置
4.1.首先看一下Config类的源代码。
/* * Copyright 2017 GcsSloop * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Last modified 2017-03-28 04:48:02 * * GitHub: https://github.com/GcsSloop * Website: http://www.gcssloop.com * Weibo: http://weibo.com/GcsSloop */ package com.gcssloop.diycode.utils; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.LruCache; import com.gcssloop.diycode_sdk.utils.ACache; import java.io.Serializable; /** * 用户设置 */ public class Config { private static int M = 1024 * 1024; private volatile static Config mConfig; private static LruCache<String, Object> mLruCache = new LruCache<>(1 * M); private static ACache mDiskCache; private Config(Context context) { mDiskCache = ACache.get(context, "config"); } public static Config init(Context context) { if (null == mConfig) { synchronized (Config.class) { if (null == mConfig) { mConfig = new Config(context); } } } return mConfig; } public static Config getSingleInstance() { return mConfig; } //--- 基础 ----------------------------------------------------------------------------------- public <T extends Serializable> void saveData(@NonNull String key, @NonNull T value) { mLruCache.put(key, value); mDiskCache.put(key, value); } public <T extends Serializable> T getData(@NonNull String key, @Nullable T defaultValue) { T result = (T) mLruCache.get(key); if (result != null) { return result; } result = (T) mDiskCache.getAsObject(key); if (result != null) { mLruCache.put(key, result); return result; } return defaultValue; } //--- 浏览器 --------------------------------------------------------------------------------- private static String Key_Browser = "UseInsideBrowser_"; public void setUesInsideBrowser(@NonNull Boolean bool) { saveData(Key_Browser, bool); } public Boolean isUseInsideBrowser() { return getData(Key_Browser, Boolean.TRUE); } //--- 首页状态 ------------------------------------------------------------------------------- private String Key_MainViewPager_Position = "Key_MainViewPager_Position"; public void saveMainViewPagerPosition(Integer position) { mLruCache.put(Key_MainViewPager_Position, position); } public Integer getMainViewPagerPosition() { return getData(Key_MainViewPager_Position, 0); } //--- Topic状态 ------------------------------------------------------------------------------ private String Key_TopicList_LastPosition = "Key_TopicList_LastPosition"; private String Key_TopicList_LastOffset = "Key_TopicList_LastOffset"; public void saveTopicListState(Integer lastPosition, Integer lastOffset) { saveData(Key_TopicList_LastPosition, lastPosition); saveData(Key_TopicList_LastOffset, lastOffset); } public Integer getTopicListLastPosition() { return getData(Key_TopicList_LastPosition, 0); } public Integer getTopicListLastOffset() { return getData(Key_TopicList_LastOffset, 0); } private String Key_TopicList_PageIndex = "Key_TopicList_PageIndex"; public void saveTopicListPageIndex(Integer pageIndex) { saveData(Key_TopicList_PageIndex, pageIndex); } public Integer getTopicListPageIndex() { return getData(Key_TopicList_PageIndex, 0); } //--- News状态 ------------------------------------------------------------------------------ private String Key_NewsList_LastScroll = "Key_NewsList_LastScroll"; public void saveNewsListScroll(Integer lastScrollY) { saveData(Key_NewsList_LastScroll, lastScrollY); } public Integer getNewsLastScroll() { return getData(Key_NewsList_LastScroll, 0); } private String Key_NewsList_LastPosition = "Key_NewsList_LastPosition"; public void saveNewsListPosition(Integer lastPosition) { saveData(Key_NewsList_LastPosition, lastPosition); } public Integer getNewsListLastPosition() { return getData(Key_NewsList_LastPosition, 0); } private String Key_NewsList_PageIndex = "Key_NewsList_PageIndex"; public void saveNewsListPageIndex(Integer pageIndex) { saveData(Key_NewsList_PageIndex, pageIndex); } public Integer getNewsListPageIndex() { return getData(Key_NewsList_PageIndex, 0); } }
4.2.定义的成员变量
M是数据单位的意思。
volatile关键字:
LruCache是android系统的通用类,存放类似map类型的缓存数据。
ACache是这个项目的SDK中定义最底层的缓存类。
4.3.构造函数+初始化+获取单例
4.4.基础是保存数据和获取数据,泛型好处理任何类型
在自定义的mLruCache和SDK包中的mDiskCache都要保存key,value之间的对应关系。
4.5.是否保存浏览器中用户数据+首页是否保存第几个碎片
估计应该就是配置是否保存一些东西吧。
4.6.话题的页码和上一个位置
获取上一个话题的位置和上一个话题的offset。
4.7.news的页码和上一个位置
获取上次滑动的位置,上一个news列表位置,上一个页面索引。
5.总结一下
5.1.以前不知道内存泄漏的严重性,当初学良让我注意android的内存泄漏的问题,我不以为然,现在看到别的项目无一
不在解决内存泄漏的问题,而且这个名字听起来内心就起疙瘩。还好LeakCanary出来了,可以解决这个大问题,也
是大难点,就不用到处寻找bug了,只要发生了内存泄漏,那个源头必然出现。
5.2.CrashHandler同样也是找bug能手,类似于腾讯的bugly,当然这个没有腾讯bugly强大。这个只能将异常输入到
一个日志,而bugly可以直接在网上输出,并且会发送邮箱,当然这个CrashHandler很简单,不用配置什么东西,
也许bugly就是根据这个来做出来的吧。
5.3.现在明白了Config是什么意思了,其实就是类似于我曾经定义的常量吧。不过这个不是常量,是一个记录的变量,
比如记录当前滑动的位置,那么下次进来就能记住了。从某个角度看,有点类似于sharePerference记录一样,
总之就是存储一些信息,然后之后来调用。
5.4.今天看的这个是Application,这是项目的大门,所以很多初始化的工作都是这里完成的。包括开启内存泄漏的检测
,开启异常日志的输出,用户设置的配置,Diycode项目SDK的初始化等。这个内存泄漏用的是第三方开源库,
记住就好,对于内存泄漏也有了更深的理解。无非就是在活动中添加一个回收即可。