美文网首页
【值得收藏】Android 性能优化之启动优化解析

【值得收藏】Android 性能优化之启动优化解析

作者: 安安_660c | 来源:发表于2023-01-28 16:24 被阅读0次

    应用启动分类:

    1. 冷启动:耗时最多、衡量标准。

      ClickEvent->IPC->Process.start->ActivityThread->bindApplication->LifeCycle->ViewRootImpl

    2. 热启动:最快,后台->前台

    3. 温启动:较快

    冷启动的相关任务

    • 冷启动之前:

      启动App -> 加载空白Window-> 创建进程

    • 随后任务:

      创建Application -> 启动主线程 -> 创建入口Acitivity -> 加载布局 -> 布置屏幕 -> 首帧绘制

    优化方向:

    Application和Activity生命周期

    1.1 启动时间测量方式

    1.1.1 adb命令

    adb shell am start -W packagename/首屏Activity

    image.png

    Status:ok(启动无异常) LaunchState:COLD(冷启动) Activity:.SplashActivity(目标Activity)

    TotalTime:所有Activity启动耗时 ThisTime:最后一个Activity启动耗时 WaitTime:AMS启动Activity的总耗时

    当前为application->SplashActivity,中间无中转Activity,因此TotalTime与ThisTime一致。

    线下使用,不能带到线上 非严谨、精确时间

    1.1.2 手动打点

    启动时埋点,启动后结束埋点,二者差值即为启动时间。

    从Application的attachBaseContext()为起点。

    误区:以onWindowsFocusChanged为首帧时间,其实这个回调只是Activity的首帧时间,实际上并不代表已绘制至屏幕中。

    正解:真实数据展示,Feed的第一条展示的时候作为结束时间。

    public class StarTimeUtil {
        private static final String TAG = "StartUtil";
        static long startTime;
        static long time;
    
        public static void startRecord() {
            startTime = System.currentTimeMillis();
        }
    
        public static void endRecord() {
            time = System.currentTimeMillis() - startTime;
            Log.d(TAG, "耗时:" + time + "ms");
        }
    }
    
    
    @Override
    protected void attachBaseContext(Context base) {
        StarTimeUtil.startRecord();
        MultiDex.install(this);
    }
    
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setContentView(R.layout.activity_splash);
        findViewById(R.id.fl).getViewTreeObserver().addOnDrawListener(onDrawListener);
    }
    
    ViewTreeObserver.OnDrawListener onDrawListener =  new ViewTreeObserver.OnDrawListener() {
        @Override
        public void onDraw() {
            StarTimeUtil.endRecord();
            findViewById(R.id.fl).post(new Runnable() {
                @Override
                public void run() {
                    findViewById(R.id.fl).getViewTreeObserver().removeOnDrawListener(onDrawListener);
                }
            });
        }
    };
    
    
    image.png

    精确,可带到线上,推荐使用。 注意避开误区,采用Feed第一条展示。 addOnDrawListener 要求 API16,版本低于16可选择使用addOnPreDrawListener。

    1.2 启动优化工具选择

    1.2.1 traceView

    图形化展示执行时间、调用栈等,信息全面,包含所有线程。

    缺点:运行时开销严重,程序整体变慢,可能会带偏优化方向。

    使用方式:

    Debug.startMethodTracing("");

    Debug.stoptMethodTracing();

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

    image.png image.png

    WallClockTime:代码执行的所需时间,包括阻塞耗时如等待锁时

    ThreadTime:CPU执行时间,CPU确实在该线程工作的时间

    CallChart: 由上至下即是调用者到被调用者,橙色为系统api的调用,绿色为应用自身api的调用,蓝色为第三方api的调用

    FlameChart: 收集重复调用的函数之类的

    TopDown: 很清晰的函数调用列表,可右键跳转到详细代码中

    Total:函数执行总时间

    self:函数自身代码执行时间

    Children:函数内部所调用函数的执行时间

    Total=self+Children

    BottomUp: 也是函数调用列表,只不过与TopDown的展示相反

    TraceView是Android平台一个很好的性能分析的工具,能够以图形的形式显示跟踪日志,但是已弃用。另外TraceView的性能消耗太大,得到的结果不真实。

    1.2.2 systrace

    结合Android内核的数据,生成Html报告。

    需要API18以上,推荐使用TraceCompat向下兼容。

    轻量级,开销小,直观反映CPU利用率。

    使用方式:

    python环境

    python systrace.py -t 10 [other-options] [categories]

    python .../Library/Android/sdk/platform-tools/systrace/systrace.py -t 5 -a com.xxx.xxx.xxx -o performance.html sched gfx view wm am app

    image.png image.png

    1.2.3 Systrace + 函数插桩

    Systrace 允许你收集和检查设备上运行的所有进程的计时信息。它包括AndroidKernel的一些数据(例如CPU调度程序,IO和App Thread),并且会生成HTML报告,方便用户查看分析trace内容。但是不支持应用程序代码的耗时分析,如果需要分析程序代码的执行时间,那就要结合函数插桩的方式,对细节进行分析。

    1.2.4 CPU Profiler

    代替Traceview的,便是CPU Profiler。

    它可以检查通过使用Debug类对应用进行插桩检测而捕获的.trace 文件、记录新方法跟踪信息、保存.trace 文件以及检查应用进程的实时CPU使用情况。

    具体使用方式,与Traceview大同小异。

    1.3 优雅获取方法耗时

    1.3.1 常规方式(手动埋点)

    long time = System.currentTimeMills();
    
    long cost =System.currentTimeMills()-time;
    
    

    或者

    // cpu真正所耗时间
    SystemClock.currentThreadTimeMills();
    
    
    long time = System.currentTimeMillis();
    initThirdPart();
    long cost = System.currentTimeMillis() - time;
    
    time = System.currentTimeMillis();
    initThirdPart();
    cost = System.currentTimeMillis() - time;
    
    time = System.currentTimeMillis();
    initThirdPart();
    cost = System.currentTimeMillis() - time;
    
    
    

    这种常规获取方法耗时的方式就很不合理,侵入性强、工作量大。

    1.3.2 AOP方式

    Aspect Oriented Programing,面向切面编程。

    应用场景:针对同一类问题的统一处理时。

    优点:无侵入添加代码,修改简单。

    使用AspectJ辅助实现

    1.3.2.1 JoinPoints

    程序运行时的执行点,可以作为切面的地方:

    • 函数调用、执行,获取
    • 设置变量
    • 类初始化

    1.3.2.2 PointCut

    带条件的JoinPoints

    1.3.2.3 Advice

    一种Hook,要插入代码的位置。

    • Before:PointCut之前执行

    • After:PointCut之后执行

    • Around:PointCut之前、之后分别执行。

    • execution:处理JoinPoint的类型:call、execution

    onActivityCalled:要插入的代码。

    @Aspect
    public class PerformanceAop {
        @Around("call(* debug.DemoApplication.**(..))")
        public void getTime(ProceedingJoinPoint joinPoint) {
            Signature signature = joinPoint.getSignature();
            String name = signature.toShortString();
            long time = System.currentTimeMillis();
            try {
                joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
    
    //        LogUtils.i(name + " cost " + (System.currentTimeMillis() - time));
        }
    
        @Around("execution(* android.app.Activity.setContentView(..))")
        public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {
    
            Signature signature = joinPoint.getSignature();
            String name = signature.toShortString();
            long time = System.currentTimeMillis();
            try {
                joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
            LogUtils.i(name + " cost " + (System.currentTimeMillis() - time));
        }
    }
    
    

    1.3 异步优化

    核心思想:子线程分担主线程任务,并行减少时间.

    需要注意:不符合异步优化的,需要在某阶段完成的,要区分CPU密集型和IO密集型任务。

    常规初始化流程

    public class DemoApplication extends BaseApplication {
    
        @Override
        public void onCreate() {
            super.onCreate();
            initThirdPart();
            initBugly();
        }
    
        private void initBugly() {
    
        }
    
        /**
         * 模拟为在Application中的初始化操作的耗时行为
         */
        private void initThirdPart() {
    
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                ToastUtils.showShortToast("第三方框架初始化耗时异常");
            }
        }
    }
    
    

    常规异步优化

    public class DemoApplication extends BaseApplication {
    
        private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    
        private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    
        private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
        private CountDownLatch mCountDownLatch = new CountDownLatch(1);
    
        @Override
        public void onCreate() {
            super.onCreate();
    
    //      关于异步优化,一个初始化对应一条线程效果更好,而有的初始化并不适合在子线程中进行,
    //      比如Handler,当然也可以改造成可以在子线程进行初始化,但是有的只能在主线程中初始化
    //      异步优化无法保证初始化的完成时机,若依然还是需要在子线程进行初始化,可以借助CountDownLatch完成
    
            ExecutorService executorService = Executors.newFixedThreadPool(CORE_POOL_SIZE);
    
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    initThirdPart();
                }
    
            });
    
            executorService.submit(new Runnable() {
    
                @Override
                public void run() {
                    initBugly();
                    mCountDownLatch.countDown();//初始化完成
    
                }
            });
            mCountDownLatch.await();//等待条件满足
    
        }
    
        private void initBugly() {
    
        }
    
        /**
         * 模拟为在Application中的初始化操作的耗时行为
         */
        private void initThirdPart() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                ToastUtils.showShortToast("第三方框架初始化耗时异常");
            }
        }
    }
    
    

    1.4 异步优化最优解

    常规异步的缺点:代码不优雅,场景不好处理(有的初始化任务会存在依赖关系),维护成本高

    启动器优化

    核心思想:充分利用CPU多核,自动梳理任务顺序

    启动器流程:

    • 代码Task化,启动逻辑抽象为Task;
    • 根据所有任务依赖关系排序生成一个有向无环图;
    • 多线程按照排序后的优先级依次执行
    image.png

    主线程:相当于常规初始化

    并发:相当于异步初始化

    head task:主体task执行前的操作

    tail task:主体task执行后的操作

    ilde task:空闲时执行的操作

    这三个task起到锦上添花的作用,可按需使用

            // 使用启动器的方式进行初始化优化
            TaskDispatcher.init(this);
            TaskDispatcher taskDispatcher = TaskDispatcher.createInstance();
            taskDispatcher.addTask(new InitBugly()).addTask(new InitThirdPartTask()).start();
            // 有的task需要等待完成
            taskDispatcher.await();
    
    

    1.5 更优秀的延迟初始化

    public interface OnFeedShowCallBack {
        void onFeedShow();
    }
    
    
    public class PerformanceOptimizationActivity extends BaseActivity implements OnFeedShowCallBack {
        ...
        ...
        ...
    
        @Override
        public void onFeedShow() {
            ...
            ...
            ...
            
            // 一系列操作 耗时十秒
            new DispatchRunnable(new DelayInitTaskA()).run();
            new DispatchRunnable(new DelayInitTaskB()).run();
        }
    
    }
    
    
    public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
        ...
        ...
        ...
    
        if (position == 0 && !mHasRecorded) {
            mHasRecorded = true;
            holder.layout.getViewTreeObserver()
                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                        @Override
                        public boolean onPreDraw() {
                            holder.layout.getViewTreeObserver().removeOnPreDrawListener(this);
                            LogUtils.i("FeedShow");
                            LaunchTimer.endRecord("FeedShow");
                            if (mCallBack != null) {
                                mCallBack.onFeedShow();
                            }
                            return true;
                        }
                    });
        }
    }
    
    

    针对以上场景时:

    1. 常规方案:

      new Handler().postDelayed,Feed展示后调用

      时机不便控制导致Feed卡顿

    2. 更优方案

      核心思想:对延迟任务进行分批初始化

      利用IdleHandler特性,空闲执行

      执行时机明确,缓解Feed卡顿

      public class PerformanceOptimizationActivity extends BaseActivity implements OnFeedShowCallBack {
          
          ...
          ...
          ...
      
          @Override
          public void onFeedShow() {
              DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
              delayInitDispatcher.addTask(new DelayInitTaskA()).addTask(new DelayInitTaskB()).start();
          }
      
      }
      
      

    1.6 全民视觉优化

    在用户点击手机桌面APP的时候,看到的黑屏或者白屏其实是界面渲染前的第一帧,解决这个问题非常轻松,无非就是将Theme里的windowBackground设置成我们想要让用户看到的画面就可以了,这里有2种做法:

    1.将背景图设置成APP的Logo图,作为APP启动的引导,现在市面上大部分的APP也是这么做的。

    <style name="AppWelcome" parent="AppTheme">
    <item name="android:windowBackground">@mipmap/bg_welcome_start</item>
    </style>
    
    

    2.将背景颜色设置为透明色,这样当用户点击桌面APP图片的时候,并不会"立即"进入APP,而且在桌面上停留一会,其实这时候APP已经是启动的了,只是我们心机的把Theme里的windowBackground的颜色设置成透明的,强行把锅甩给了手机应用厂商。

    <style name="Appwelcome" parent="android:Theme.Translucent.NoTitleBar.Fullscreen"/>
    
    

    透明化这种做法需要注意的一点,如果直接把Theme引入Activity,在运行的时候可能会出现如下异常:

    java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.

    这个是因为使用了不兼容的Theme,例如我这里的Activity继承了AppCompatActivity,解决方案很简单: 1、让其Activity集成Activity而不要集成兼容性的AppCompatActivity 2、在onCreate()方法里的super.onCreate(savedInstanceState)之前设置我们原来APP的Theme。

    public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    setTheme(R.style.AppTheme);
    super.onCreate(savedInstanceState);
    }
    }
    
    

    1.7 启动优化的其它方案

    优化总方针:异步、延迟、懒加载,技术、业务相结合。

    收敛启动代码的的修改权限:结合CI,修改启动代码需要Review或通知,防止维护好的启动启动代码被破坏。

    1. 提前加载SharedPreferences

      SharedPreferences是IO操作,可在Multidex之前加载,利用此阶段的CPU,此时的CPU是肯定尚未跑满的。SharedPreferences是系统类,在这之前提前加载初始化SharedPreferences不会有异常,是少有的可以在Multidex之前加载优化的类。

      覆写getApplicationContext(),返回this。

    2. 启动阶段不启动子进程

      子进程会共享CPU资源。

      注意启动顺序:App onCreate之前是ContentProvider。

    3. 类加载优化:提前异步类加载

      Class.forName()只加载类本身及其静态变量引用类。

      new 类实例,可以额外加载类成员变量的引用类。

    4. 启动阶段抑制GC

    5. CPU锁频

    2. 造火箭攻略

    2.1 自问自答

    你的启动优化是怎么做的?

    是启动优化的负责人

    分析现状、确认问题

    比如启动速度的监听、竞品分析

    针对性优化:思路从小到大

    长期保持优化效果:启动器代码的封装;收敛启动代码的修改权限

    是怎么异步的,异步遇到问题没有?

    体现演进过程:线程池->启动器

    详细介绍启动器

    你做了启动优化,觉得有哪些容易忽略的注意点?

    cpu time与wall time的区别,cpu time是优化方向

    注意延迟初始化的优化:不用常规方式,用idleHandler

    介绍下黑科技:类加载,抑制GC,CPU锁频

    版本迭代导致的启动变慢,有好的解决方式吗?

    启动器

    结合CI

    新加代码后,上线前就先及时测量启动速度,监控完善

    2.2 Baseline Profiles

    3. 拧螺丝攻略

    3.1 启动优化预备知识

    3.1.1 Wall Duration 与 CPU Duration

    image.png

    Wall Duration:代码执行时间 CPU Duration:代码消耗CUP的时间(重点指标,优化方向)

    比如锁的冲突:线程执行的方法本身也许并不耗时,但是由于要等待锁的释放,而这便会加长代码的执行时间,等待锁处于阻塞的过程,CPU并不会在在该线程的方法上消耗时间。

    3.1.2 adb命令

    获取当前APP的包名类名: adb shell "dumpsys window | grep mCurrentFocus

    adb启动指定activity: adb shell am start -W [包名]/[类名]

    adb shell am start -S -R 5-W [包名]/[类名]

    -S:表示每次启动前先强行停止

    -R:表示重复测试次数

    adb查看相关进程信息(模糊匹配): adb shell "ps|grep [模糊匹配进程名]"

    adb杀死进程: adb shell kill [PID]/adb shell am force-stop [包名]

    链接:https://juejin.cn/post/7096013265179770910
    作者:复制粘贴改改改

    相关文章

      网友评论

          本文标题:【值得收藏】Android 性能优化之启动优化解析

          本文链接:https://www.haomeiwen.com/subject/ubzyhdtx.html