性能优化-稳定性优化

稳定性优化

稳定性中两个常见场景:Crash和ANR

提高代码质量

代码审查

  1. 何时审查
    分两个方面:一是这个模块是否需要审查,明确代码审查的必要性,二是在开发阶段的哪个时间点代码审查,确定审查合理时间点。

一般审查:底层公共模块、重大特性业务代码、与其他模块有耦合、新手、应用即将发布前的紧急修改。

  1. 谁来审查

代码审查分为三种方式

  • 团队审查:底层通用模块
  • 模块负责人审查:某些模块化的功能和业务
  • 结对审查:两人结对互相审查
  1. 审查内容

审查的流程:先审查设计实现思路,然后审查设计模式,接着审查形成的骨干代码,然后审查完成的代码。

审查内容:

  • 实现思路和设计思想
  • 代码设计
  • 设计逻辑
  • 代码风格
  • 需求理解

代码静态扫描工具

4种常用Java代码分析工具对比如下


image.png
  1. Checkstyle:通过对代码编码格式、命名约定、Javadoc、类she j设计等方面进行代码规范和风格的检查。
  2. FindBugs:通过检测类文件或JAR文件,将字节码与一组缺陷模式进行对比从而发现代码缺陷,完成静态代码分析。
  3. PMD:通过其内置的编码规则对Java代码进行静态检查,主要检查潜在的bug、未使用的代码、重复的代码、循环体创建新对象等问题。
  4. Android Lint:除了代码缺陷,还能检测代码布局的合理性。

Crash监控

Android应用中发生的crash有两种类型,Java层的Crash和Native层的Crash

Java层的Crash监控

Android中,Java虚拟机为每个进程都设置类一个默认的UncaughtExeptionHandler,用于处理本进程中未被try catch的Exception。因此只有实现UncaughtExeptionHandler接口,并进程启动时调用Thread.setDefaultUncaughtExceptionHandler(...)传入自定义的UncaughtExeptionHandler,发生未捕获异常时就会回调uncaughtException(Thread thread, Throwable ex) 方法。

demo

public class ABLCrashHandler implements UncaughtExceptionHandler {

    public static final String TAG = "ABLCrashHandler";
    // ABLCrashHandler 实例
    private static ABLCrashHandler INSTANCE = null;
    // 程序的 Context 对象
    private Context mContext;
    // 系统默认的 UncaughtException 处理类
    private UncaughtExceptionHandler mDefaultHandler;
    // 用来存储设备信息和异常信息
    private Map<String, String> infos = new HashMap<String, String>();

    //保证只有一个 ABLCrashHandler 实例
    private ABLCrashHandler(Context context) {
        this.init(context);
    }

    //获取 ABLCrashHandler 实例 ,单例模式
    public static ABLCrashHandler getInstance(Context context) {
        if (INSTANCE == null) {
            INSTANCE = new ABLCrashHandler(context);
        }
        return INSTANCE;
    }

    /**
     * 初始化
     *
     * @param context
     */
    public void init(Context context) {
        mContext = context;
        // 获取系统默认的 UncaughtException 处理器
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        // 设置该 ABLCrashHandler 为程序的默认处理器
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    /**
     * 当 UncaughtException 发生时会转入该函数来处理
     */
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        if (!handleException(ex) && mDefaultHandler != null) {
            // 如果用户没有处理则让系统默认的异常处理器来处理
            mDefaultHandler.uncaughtException(thread, ex);
        } else {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                Log.e(TAG, "error : ", e);
            }
            // 退出程序
            android.os.Process.killProcess(android.os.Process.myPid());
            System.exit(1);
        }
    }

    /**
     * 自定义错误处理,收集错误信息,发送错误报告等操作均在此完成
     * 
     * @param ex
     * @return true:如果处理了该异常信息;否则返回 false
     */
    private boolean handleException(final Throwable ex) {
        if (ex == null) {
            return false;
        }

        ex.printStackTrace();

        //使用 Toast 来显示异常信息
        new Thread() {
            @Override
            public void run() {
                Looper.prepare();

                try {
                    ABLApplication.getApplication().getServicesManager().stopScan();
                    ABLApplication.getApplication().getServicesManager().destroy();

                    StringBuffer sb = new StringBuffer();
                    for (Map.Entry<String, String> entry : infos.entrySet()) {
                        String key = entry.getKey();
                        String value = entry.getValue();
                        sb.append(key + "=" + value + "\n");
                    }
                    Writer writer = new StringWriter();
                    PrintWriter printWriter = new PrintWriter(writer);
                    ex.printStackTrace(printWriter);
                    Throwable cause = ex.getCause();
                    while (cause != null) {
                        cause.printStackTrace(printWriter);
                        cause = cause.getCause();
                    }
                    printWriter.close();
                    String message = writer.toString();

                    Log.i(TAG + ".handleException.message", message + " |");

                    String exceptionCode = ABLExceptionCoder.getExceptionCode(message);

                    Log.i(TAG + ".handleException.exceptionCode", exceptionCode + " |");

                    Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出,错误码:" + exceptionCode, Toast.LENGTH_LONG).show();

                } catch (Exception e) {
                    e.printStackTrace();
                }

                Looper.loop();
            }
        }.start();
        // 收集设备参数信息
        collectDeviceInfo(mContext);
        // 保存日志文件
        saveCrashInfo2File(ex);
        return true;
    }

    /**
     * 收集设备参数信息
     * @param ctx
     */
    public void collectDeviceInfo(Context ctx) {
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);

            if (pi != null) {
                String versionName = pi.versionName == null ? "null" : pi.versionName;
                String versionCode = pi.versionCode + "";
                infos.put("versionName", versionName);
                infos.put("versionCode", versionCode);
                infos.put("deviceID", Utils.getDeviceID_B());
            }
        } catch (NameNotFoundException e) {
            Log.e(TAG, "an error occured when collect package info", e);
        } catch (Exception e) {
            Log.e(TAG, "an error occured when collect package info", e);
        }

        Field[] fields = Build.class.getDeclaredFields();
        for (Field field : fields) {
            try {
                field.setAccessible(true);
                infos.put(field.getName(), field.get(null).toString());
                Log.d(TAG, field.getName() + " : " + field.get(null));
            } catch (Exception e) {
                Log.e(TAG, "an error occured when collect crash info", e);
            }
        }
    }

    /**
     * 保存错误信息到文件中
     *
     * @param ex
     * @return  返回文件名称,便于将文件传送到服务器
     */
    public String saveCrashInfo2File(Throwable ex) {
        try {
            StringBuffer sb = new StringBuffer();
            for (Map.Entry<String, String> entry : infos.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
                sb.append(key + "=" + value + "\n");
            }
            Writer writer = new StringWriter();
            PrintWriter printWriter = new PrintWriter(writer);
            ex.printStackTrace(printWriter);
            Throwable cause = ex.getCause();
            while (cause != null) {
                cause.printStackTrace(printWriter);
                cause = cause.getCause();
            }
            printWriter.close();
            String result = writer.toString();

            String exceptionCode = ABLExceptionCoder.getExceptionCode(result);

            sb.append("exceptionCode=" + exceptionCode + "\n");

            sb.append(result);
            try {
                String fileName = "crash-" + DateUtil.getNowTime3() + ".log";
                if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                    String path = FileUtils.getSDCardPath() + ABLConfig.SAVE_FOLDER + "/Bug/";
                    File dir = new File(path);
                    if (!dir.exists()) {
                        dir.mkdirs();
                    }
                    File file = new File(path + fileName);
                    if  (!file.exists()){
                        file.createNewFile();
                    }

                    //增加异常信息
                    FileWriter fw = new FileWriter(file, true);
                    fw.write("time:" + DateUtil.getNowTime());
                    fw.write("\r\n");
                    fw.write(sb.toString());
                    fw.write("\r\n\r\n");
                    fw.flush();
                    fw.close();
                }
                return fileName;
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, "an error occured while writing file...", e);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

Nativa 层Crash监控

当应用程序发生异常时,Linux内核会生成错误信号并通知当前进程。应用进程接受到错误信号后,可以捕获该信号并执行对应的信号处理函数。而当应用发生验证错误时后发生Crash,Linux有一类专门用于描述Crash的信号。只要在应用程序注册了这些信号的处理函数,当JNI crash时,我们的处理函数就会被调用到,然后获取dump文件再上传,后续的工作就和Java异常逻辑一致了。

ANR剖析

ANR介绍

类型:

  1. KeyDispatchTimeout:对输入事件5秒内无响应
  2. BroadcaseTimeout:在指定时间(默认10秒)无法处理完毕,并且没有结束执行onReceive
  3. ServiceTimeout:指Service在特定时间(默认20秒)内无法处理完成。

ANR分析

如果发送ANR, Logcat会产生对应的日志和一个traces文件,这个文件保存在/data/anr/traces.txt。
可以直接用adb工具获取traces.txt文件:

adb pull /data/anr/traces.txt

可以通过分析log和traces文件信息定位到anr发生的位置和分析原因。

AS提供了一个分析trace文件的工具:Analyze Stacktrace。打开Analyze Stacktrace工具,将traces.txt文件内容复制到窗口,单击Normalize按钮,生成Thread Dump列表。如果某个线程被标红,说明此线程被堵塞了。

提高后台进程存活率

当内存紧张时,优先级低、占用内存大的app进程会优先被杀死。可以提高提高进程优先级使应用在后台的存活时间更长,一般使用以下几种方法实现:

  1. 网络连接

通过长连接心跳和进程保持通信,使进程保持活动状态,但如果系统内存非常紧张,也有可能被杀。

  1. 利用系统现有机制

一般可以注册系统消息(AlarmReceive、BootReceive等),通过系统消息响应挂起进程。

  1. SyncAdapter

利用Android系统提供的账号同步机制实现进程优先级提高。

SyncAdapter是一个系统服务,通过系统的定时器更新应用程序数据ContentProvider,因为Sync服务工作在独立进程,并由系统调度,属于核心进程级别,系统不会杀掉,而使用了SyncAdapter的进程优先级也会提高,优先级变为1,仅低于前台正在运行的进程,因此可以降低应用被系统杀掉概率。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容