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);
    }
}
View Code

 

1.2.代码预览

  

  首先是两个静态变量,是客户的的id和密钥。

  然后是一个onCreate函数,进行一些必要的初始化。

 

1.3.然后调用Application的onCreate函数  

  

  这里用了一个第三方库LeakCanary。

  首先install这个application

  这里的CrashHandler的作用是崩溃处理办法。

  然后就用将上面的客户的id和客户的密码来初始化了。

  用户设置Config初始化也是这里进行。


2.LeakCanary内存泄露检测

2.1.首先了解一下什么是LeakCanary。

  LeakCanary是检测APP内存泄露的工具,内存泄漏是Android开发中常用的问题,导致程序的稳定性下降。

  github原地址点我。

  参考文章:使用Leak Canary检测应用的内存泄漏。

 

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,每次异常,它都会给我发送一封邮件,告诉我哪里崩溃,

  哪里异常,每天早上也会统计今日崩溃次数等。非常简单实用的第三方库。

  这里提供一下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
    }

}
View Code

 

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);
    }
}
View Code

 

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的初始化等。这个内存泄漏用的是第三方开源库,

  记住就好,对于内存泄漏也有了更深的理解。无非就是在活动中添加一个回收即可。

 


 

posted @ 2017-11-22 14:58  Jason_Jan  阅读(764)  评论(0编辑  收藏  举报