新闻类App (MVP + RxJava + Retrofit+Dagger+ARouter)性能优化之APP卡顿优化

Github地址:新闻类App (MVP + RxJava + Retrofit+Dagger+ARouter)

卡顿介绍以及优化工具选择

背景介绍:
很多性能问题不易被发现,但是卡顿很容易被直观发现,且卡顿难以定位

CPU Profiler

  • 图形的形式展示执行时间,调用栈等
  • 信息全面,包含所有的线程
  • 缺点:运行时开销严重,整体都会变慢
  • 使用方式
Debug.startMethodTracing("")
Debug.stopMethodTracing("")

生成的文件在sd卡:Android/data/packagename/files

systrace

  • 监控和跟踪 API调用,线程运行情况,生成HTML报告
  • API18以上使用,推荐TraceCompat
  • 使用方式
python systrace.py -t 10 [other-options] [categories]
  • 优点
    轻量级,直观反映CPU利用率,给出建议

StrictMode

  • 严苛模式,Andorid提供的一种运行时检测机制
  • 方便强大,容易被忽视
  • 包含:线程策略和虚拟机策略检测
    线程策略
    自定义耗时调用,detectCustomSlowCalls();
    磁盘读取操作,detectDiskReads()
    网络操作,detectNetwork
    虚拟机策略
    Activity泄漏,detectActivityleaks()
    Sqlite对象泄漏,detectleakedSqliteObjects
    检测实例数量,setClassInstanceLimit()
  • 代码
 private boolean DEV_MODE = true;

    private void initStrictMode() {
        if (!DEV_MODE) {
            //线程策略
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectCustomSlowCalls()//API等级11,使用StrictMode.noteSlowCode
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()
                    .penaltyLog()//在Logcat 中打印违规异常信息
                    .build());
            //虚拟机策略
            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                    .detectLeakedSqlLiteObjects()
                    //模拟限制数量1
                    .setClassInstanceLimit(NewsTimeLine.class, 1)
                    .detectLeakedClosableObjects() //API等级11
                    .penaltyLog()
                    .build());
        }
    }

自动化卡顿检测方案及优化

理由:

  • 系统工具适合线下针对行分析
  • 线上及测试环境需要自动化检测方案

方案原理

  • 消息处理机制,一个线程只有一个Looper
  • mLogging对象在每个message处理前后被调用
  • 主线程发生卡顿,是在dispatchMessage执行耗时操作

Loop源码

            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
       if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

具体实现

  • Looper.getMainLooper().setMessagelogging()
  • 匹配>>>>> Dispatching,阈值时间后执行任务(获取堆栈)
  • 匹配<<<<< Finished,任务启动之前取消掉

AndroidPerformanceMonitor

  compile 'com.github.markzhai:blockcanary-android:1.5.0'
  • 代码
    App的onCreate中
  BlockCanary.install(this, new AppBlockCanaryContext()).start();

AppBlockCanaryContext github中作者提供了

public class AppBlockCanaryContext extends BlockCanaryContext {


    @Override
    public String provideQualifier() {
        return "unknown";
    }

    @Override
    public String provideUid() {
        return "uid";
    }


    @Override
    public String provideNetworkType() {
        return "unknown";
    }

    @Override
    public int provideMonitorDuration() {
        return -1;
    }


    @Override
    public int provideBlockThreshold() {
        return 500;
    }

    @Override
    public int provideDumpInterval() {
        return provideBlockThreshold();
    }


    @Override
    public String providePath() {
        return "/blockcanary/";
    }


    @Override
    public boolean displayNotification() {
        return true;
    }


    @Override
    public boolean zip(File[] src, File dest) {
        return false;
    }


    @Override
    public void upload(File zippedFile) {
        throw new UnsupportedOperationException();
    }



    @Override
    public List<String> concernPackages() {
        return null;
    }


    @Override
    public boolean filterNonConcernStack() {
        return false;
    }


    @Override
    public List<String> provideWhiteList() {
        LinkedList<String> whiteList = new LinkedList<>();
        whiteList.add("org.chromium");
        return whiteList;
    }

   @Override
    public boolean deleteFilesInWhiteList() {
        return true;
    }


    @Override
    public void onBlock(Context context, BlockInfo blockInfo) {
        Log.i("lz","blockInfo "+blockInfo.toString());
    }
}

在fragment中添加睡眠两秒

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

结果


image.png

点击第一个


image.png

-优势:非侵入式,方便精确到哪一行

  • 缺点:确实卡顿了,但卡顿堆栈可能不准确,和OOM一样,最后的堆栈只是表象,不是真正的问题
  • 优化
    获取监控周期内的多个堆栈,而不仅仅是最后一个
    startMonitor->高频采集堆栈->endMonitior->记录多个堆栈->上报
  • 海量卡顿堆栈处理
    分析:一个卡顿下多个堆栈大概率有重复
    解决:对一个卡顿下堆栈进行hash排查,找出重复的堆栈
    效果:极大的减少展示量同时更高效的找到卡顿堆栈

ANR分析与实战

ANR产生的条件

  • 1.主线程
  • 2.超时时间
    产生ANR的上下文不同,超时时间也会不同
  • 3、输入事件/特定操作
    输入事件是指按键、触屏等设备输入事件
    特定操作是指BroadcastReceiver和Service的生命周期中的各个函数

ANR产生的情况

  • 1、主线程对输入事件在5秒内没有处理完毕
  • 2、主线程在执行BroadcastReceiver的onReceive函数时10秒内没有执行完,注意前台10s,后台60s
  • 3、主线程在执行Service的各个生命周期函数时20秒内没有执行完毕,注意前台20s,后台200s

ANR执行流程

  • 发生ANR
  • 进程接受异常终止信号,开始写入进程ANR信息
  • 弹出ANR提示框

分析ANR
ANR信息保存在在/data/anr/traces.txt中

image.png

将目录下的文件导出

Traces.txt文件分析

//文件中输出的第一个进程的trace信息,正是发生ANR的程序
//开头显示进程号、ANR发生的时间点和进程名称
----- pid 2226 at 2019-01-08 22:02:22 -----
Cmd line: com.peakmain.testproject

以下是各个线程的函数堆栈信息


image.png
//依次是:线程名、线程优先级、线程创建时的序号、线程当前状态
"main" prio=5 tid=1 Sleeping
 //主线程信息
  at java.lang.Thread.sleep!(Native method)
  - sleeping on <0x0bf9c149> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:371)
  - locked <0x0bf9c149> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:313)
  at com.peakmain.testproject.MainActivity$2.onClick(MainActivity.java:61)

其他线程信息


image.png

ANR解决套路

  • adb pull data/anr/trance.txt存储路径,可以直接导出trace文件
  • 1、主线程需要做耗时操作的时候必须启动子线程处理
  • 2、子线程尽量使用android提供的API,比如HandlerThread,AsyncTask
  • 3、Broadcast Receiver中如果有耗时操作,可以放到service中

ANR-WatchDog

compile 'com.github.anrwatchdog:anrwatchdog:1.4.0'
  • 代码
    App的onCreate中
new ANRWatchDog().start();
  • 原理
    start->post消息改值->sleep->检测是否修改->判断ANR是否发生

卡顿单点问题检测方案

IPC问题检测

  • IPC调用类型
  • 调用耗时,次数
  • 调用堆栈,发生线程

常规方案
IPC前后埋点,缺点:不够优雅,而且维护成本高

IPC问题检测技巧

  • adb命令
    adb shell am trace -ipc start
    adb shell am trace -ipc stop ——dump-file /data/local/tmp/ipc-trace.txt
    adb pull /data/local/tmp/ipc-trace.txt

优雅的方案:ARTHook

  • 挂钩,将额外的代码钩住原有的方法,修改执行逻辑
  • 框架:Epic(不能带到线上环境)
     try {
            DexposedBridge.findAndHookMethod(Class.forName("android.os.BinderProxy"), "transact",
                    int.class, Parcel.class, Parcel.class, int.class, new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                            LogUtils.i( "BinderProxy beforeHookedMethod " + param.thisObject.getClass().getSimpleName()
                                    + "\n" + Log.getStackTraceString(new Throwable()));
                            super.beforeHookedMethod(param);
                        }
                    });
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

实现界面秒开

  • onCreate到onWindowFocusChanged消耗的时间

Lancet

  • 编译速度快,支持增量更新
  • API简单,没有任何多余代码插入apk
  • Github:https://github.com/eleme/lancet
  • API介绍
    @Proxy通常用于对系统API调用的Hook
    @Insert通常用于操作App与libray的类
  • 依赖
classpath 'me.ele:lancet-plugin:1.0.4'
apply plugin: 'me.ele.lancet'

dependencies {
    provided 'me.ele:lancet-base:1.0.4'
}
  • 代码
public class ActivityHooker {
    public static ActivityRecord sActivityRecord;
    static {
        sActivityRecord=new ActivityRecord();
    }
    @Insert(value = "onCreate",mayCreateSuper = true)
    @TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
    protected void onCreate(Bundle savedInstanceState) {
        sActivityRecord.mOnCreateTime = System.currentTimeMillis();
        Origin.callVoid();
    }


    @Insert(value = "onWindowFocusChanged",mayCreateSuper = true)
    @TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
    public void onWindowFocusChanged(boolean hasFocus) {
        sActivityRecord.mOnWindowsFocusChangedTime = System.currentTimeMillis();
        Log.i("ActivityHooker","onWindowFocusChanged cost "+(sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime));
        Origin.callVoid();
    }

    /**
     * hook系统方法
     */
    @Proxy("i")
    @TargetClass("android.util.Log")
    public static int i(String tag, String msg) {
        msg = msg + "ActivityHooker";
        return (int) Origin.call();
    }

}

监控耗时盲区

背景:

  • 生命周期的间隔
  • onResume到feed(界面数据)展示的间隔
  • 举例:postmessage,很可能在feed之前显示
  • 线下方案:tranceView
  • 线上方案
    1.主线程所有方法都经过msg,但是没有msg具体堆栈
    2.使用统一的Handler:定制具体的方法,发送消息都会走到sendMessageAtTime和处理消息都会走到dispatchMessage方法
public class PeakmainHandler extends Handler {
    private long mStartTime = System.currentTimeMillis();
    private ConcurrentHashMap<Message, String> sMsgDetail = new ConcurrentHashMap<>();

    public PeakmainHandler() {
        super(Looper.myLooper(), null);
    }

    public PeakmainHandler(Callback callback) {
        super(Looper.myLooper(), callback);
    }

    public PeakmainHandler(Looper looper, Callback callback) {
        super(looper, callback);
    }

    public PeakmainHandler(Looper looper) {
        super(looper);
    }

    @Override
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        boolean send = super.sendMessageAtTime(msg, uptimeMillis);
        if (send) {
            sMsgDetail.put(msg, Log.getStackTraceString(new Throwable()).replace("java.lang.Throwable", ""));
        }
        return send;
    }

    @Override
    public void dispatchMessage(Message msg) {
        mStartTime = System.currentTimeMillis();
        super.dispatchMessage(msg);

        if (sMsgDetail.containsKey(msg)
                && Looper.myLooper() == Looper.getMainLooper()) {
            JSONObject jsonObject = new JSONObject();
            try {
                jsonObject.put("Msg_Cost", System.currentTimeMillis() - mStartTime);
                jsonObject.put("MsgTrace", msg.getTarget() + " " + sMsgDetail.get(msg));

                Log.i("PeakmainHandler", "MsgDetail " + jsonObject.toString());
                sMsgDetail.remove(msg);
            } catch (Exception e) {
            }
        }
    }

}
  • 3.gradle定制,编译时动态替换Handler,这里我并没有去做gradle插件(理由:懒),只说下我的代码实现,这里我多写了个方法,获取所有的Handler
    写个类HandlerHelper,随后在App中初始化就可以了
public class HandlerHelper {

    public static void init() {

        try {
            //获取系统的Handler的sendMessageAtTime
            Class<?> handlerClass = Class.forName("android.os.Handler");
            Method sendMessageAtTime = handlerClass.getDeclaredMethod("sendMessageAtTime", new Class[]{Message.class, long.class});
            PeakmainHandler peakmainHandler=new PeakmainHandler();
            handlerClass=peakmainHandler.getClass();
            Object obj = Proxy.newProxyInstance(handlerClass.getClassLoader(), handlerClass.getInterfaces(), new HandlerProxy(handlerClass));
            sendMessageAtTime.invoke(handlerClass,obj);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
     private  static class HandlerProxy implements InvocationHandler {


        private  Class<?> mHandlerClass;

        public HandlerProxy(Class<?> handlerClass) {
            this.mHandlerClass=handlerClass;

        }

        @Override
        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
            return method.invoke(mHandlerClass,objects);
        }
    }
    public static List<Handler> getHandlerByApplication(Application application) {
        List<Handler> list = new ArrayList<>();
        try {
            Class<Application> applicationClass = Application.class;
            Field mLoadedApkField = applicationClass.getDeclaredField("mLoadedApk");
            mLoadedApkField.setAccessible(true);
            Object mLoadedApk = mLoadedApkField.get(application);
            Class<?> mLoadedApkClass = mLoadedApk.getClass();
            Field mActivityThreadField = mLoadedApkClass.getDeclaredField("mActivityThread");
            mActivityThreadField.setAccessible(true);
            Object mActivityThread = mActivityThreadField.get(mLoadedApk);
            Class<?> mActivityThreadClass = mActivityThread.getClass();
            Field mActivitiesField = mActivityThreadClass.getDeclaredField("mH");
            mActivitiesField.setAccessible(true);
            Object mH = mActivitiesField.get(mActivityThread);
            // 注意这里一定写成Map,低版本这里用的是HashMap,高版本用的是ArrayMap
            list.add((Handler) mH);

        } catch (Exception e) {
            e.printStackTrace();
            list = null;
        }
        return list;
    }
}

使用

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

推荐阅读更多精彩内容