Android进阶——性能优化之APP启动速度优化实战总结(三)

引言

前篇文章花了相当大的篇幅从理论和源码角度总结了APP启动背后的故事和原理,明确了我们可以优化的地方,但是要想真正实现完美的优化,得准确定位到罪魁祸首,对症下药方能药到病除,这篇就总结下实战中APP启动优化的措施和经验分享。性能优化系列文章链接:

一、确定启动时间

俗话说工欲善其事,必先利其器。如果想要优化 App 的启动速度,首先得明确知道当前Activity的启动时间,方法有多种。
这里写图片描述

1、API19之后直接使用Displayed过滤Log和配合reportFullyDrawn

1.1、DisplayTime

这里写图片描述
在API19之后Android在系统Log中自动增加了DisplayTime对应的Log信息,过滤ActivityManager以及Display这两个关键字,可以找到系统中的这个Log

$ adb logcat | grep “ActivityManager”
04-21 19:34:50.997 879-904/? I/ActivityManager: Displayed com.android.systemui/.recent.RecentsActivity: +846ms
04-21 19:35:02.277 879-904/? I/ActivityManager: Displayed com.crazymo.ubxtech/.view.activity.MainActivity: +2s514ms

这里写图片描述
通过这种方式得到时间其实Activity启动,到对应Layout全部显示的耗费的时间,但并不包括具体业务数据的加载,因为很多App在加载时会使用懒加载模式,在异步拉取数据之后,再刷新对应的界面。

1.2、reportFullyDrawn()

这里写图片描述
上面的系统日志中的Display Time只是布局的显示时间,并不包括一些数据的懒加载等消耗的时间,如果我们需要计算懒加载部分的时间,可以在UI刷新方法里手动调用reportFullyDrawn()上报时间。

public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Void> {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public void onLoadFinished(Loader<Void> loader, Void data) {
        // 加载数据
        // 上报reportFullyDrawn
        reportFullyDrawn();
    }

    @Override
    public Loader<Void> onCreateLoader(int id, Bundle args) {
        return null;
    }

    @Override
    public void onLoaderReset(Loader<Void> loader) {

    }
}

2、通过AM shell命令——adb shell am start -W 包名/包名的apk计算启动时间

本质上adb shell am start -W 命令是执行了om.android.commands.am.AM类里的相应方法,由ActivityRecord的reportLaunchTimeLocked赋值的并返回到AM的runStart方法内部(具体就是这个地方:result = mAm.startActivityAndWait(null, null, intent, mimeType,null, null, 0, mStartFlags, profilerInfo, null, mUserId),事实上这句代码执行完毕之后Activity才会显示出来)

D:\BizProject\UBXTech>adb shell am start -W com.crazymo.ubxtech/.view.activity.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.crazymo.ubxtech/.view.activity.MainActivity }
Warning: Activity not started, its current task has been brought to the front
Status: ok
Activity: com.crazymo.ubxtech/.view.activity.MainActivity
ThisTime: 602
TotalTime: 602
WaitTime: 766
Complete

此法获取的启动时间非常精准,可精确到毫秒,另外当启动时间过长(一般是超过10s,status就会为timeout,不显示thisTime和TotalTime)

  • ThisTime——最后一个启动的Activity的启动耗时
  • TotalTime—— 自己的所有Activity的启动耗时
  • WaitTime—— ActivityManagerService启动App的Activity时的总时间(包括当前Activity的onPause()和自己Activity的启动)

举个例子:1.上一个Activity的onPause()——2.系统调用AMS耗时——3.第一个Activity(也许是闪屏页)启动耗时——4.第一个Activity的onPause()耗时——5.第二个Activity启动耗时那么,ThisTime表示5(最后一个Activity的启动耗时)。TotalTime表示3.4.5总共的耗时(如果启动时只有一个Activity,那么ThisTime与TotalTime应该是一样的)。WaitTime则表示所有的操作耗时,即1.2.3.4.5所有的耗时。每次给出的时间可能并不一样,而且应用从首次安装启动到后面每次正常启动,时间都会不同,区别于系统是否要分配进程空间。

二、通过trace文件日志精准定位找出造成性能不佳的罪魁祸首

通过上面的确定时间,只能知部分Activity的启动时间,还不能精确定位到具体是哪一个方法造成的,幸运的是Android 已经内置了内置的一个工具——TraceView 它可以加载 trace 文件,用图形的形式展示代码的执行时间、次数及调用栈,便于我们分析。trace 文件是 log 信息文件的一种,可以通过代码,Android Studio或者 DDMS 生成,配合使用 Android SDK 提供的其他工具可以生成很多 log 文件,便于我们分析当前应用的内存、布局等状况

1、生成trace文件

生成trace文件的方法一共有两种:通过代码Debug.startMethodTracing和直接通过AndroidStudio的界面操作(其实还有一种可以通过DDMS来生成)

1.1、通过代码直接生成

代码很简单,当你调用开始代码Debug.startMethodTracing(“tracename”)的时候,系统会生产 trace 文件,并且产生追踪数据,当你调用结束代码Debug.stopMethodTracing()时,会将追踪数据写入到 trace 文件中,也通过Debug.startNativeTracing()和Debug.stopNativeTracing()来开始和停止追踪本地的方法,需要注意下系统版本Android 26的时候好像不能用了,调用的时候直接抛异常?

Debug.startMethodTracing("cmotrace"); //开始 trace
//一大段可疑的代码逻辑... 
Debug.stopMethodTracing(); //结束代码

1.2使用 Android Studio 生成 trace 文件

Android Studio 内置的Android Profiler可以快速生成trace文件,在 CPU 监控的那栏会有一个小红点按钮,启动应用后,点击一次后开始追踪相当于代码调用 startMethodTracing,再点一次停止追踪。

这里写图片描述

2、读懂trace文件

通过traceView我们了解到一些方法的执行时间、次数以及调用关系,也可以搜索过滤特定的内容。
这里写图片描述
另外鼠标悬浮到黄色的矩形上,会显示对应方法的开始、结束时间,以及自己占用和调用其他方法占用的时间比例。这里写图片描述

3、借助trace文件定位影响性能的罪魁祸首

所谓技巧只有一条:先找到消耗时间最长的矩形框,然后结合代码分析,这里要注意的是有时候并不是说找到最长的矩形对应的方法就可以了,还需要逐层查找,查找出到底具体是什么原因造成的,内部的某个方法还是自身的逻辑太过复杂造成的。

三、APP启动常见优化措施

通常APP 优化不宜过早,也不宜太过频繁,否则会徒增成本所以得把握好这个时间。

1、优化Activity 自身的View 的层次结构,尽量减小布局的层次,没有需要用到weight时尽量优先考虑使用Linearlayout,复杂层次考虑使用ConstraintLayout替代其他常见布局。

2、针对项目中完成一些初始化工作的静态代码区Static Block,特别是ContentProvider中在Static Block中初始化一些UriMatcher,结合具体情况使用懒加载模式。

3、尽量减轻Application的负担

Application是程序的主入口,特别是很多第三方SDK都会需要在Application的onCreate里面做很多初始化操作,再加上自己的一些库的初始化,会让整个Application不堪重负。

3.1、使用Application的一些注意事项

  • Application的构造器方法、attachBaseContext()、onCreate()方法中不要进行耗时操作的初始化,一些数据预取放在异步线程中,可以采取Callable实现

  • 对于把对sp的初始化放在异步线程中处理,因为sp的特性在初始化时候会对数据全部读出来存在内存中,所以这个初始化放在主线程中不合适,反而会延迟应用的启动速度;

  • 对于MainActivity,由于在获取到第一帧前,需要对contentView进行测量布局绘制操作,尽量减少布局的层次,考虑StubView的延迟加载策略,所以需要避免在onCreate、onStart、onResume方法中做耗时操作。

3.2、具体的优化方向和措施

除了需要在开发时候遵守一些规范,还可以从以下几个方向考虑:

  • 多线程初始化——利用多线程让Application的onCreate里面尽可能的少做事情,把一些无特殊要求的第三方框架的初始化工作放到子线程,最简单的形式直接new Thread()然后放到run()方法,当然也可以通过公共的线程池来进行异步的初始化工作,这个是最能够压缩启动时间的方式,不过有些库或SDK是必须放在主线程初始化的,这时候可以考虑延迟初始化

  • 延迟初始化 ——延迟初始化并不是减少了启动时间,而是让耗时操作让位、让资源给UI绘制,将耗时的操作延迟到UI加载完毕后,所以,这里建议通过mDecoView.post方法,来进行延迟加载,因为ContentView就是通过mDecoView.addView加入到根布局的,所以,通过这种方式,可以让延迟加载的内容,在ContentView初始化完毕后,再进行执行,保证了UI绘制的流畅性。

getWindow().getDecorView().post(new Runnable() {

  @Override 
  public void run() {
    ...耗时操作
  }
});

另一方面也可以设置为单例,然后再使用的时候再进行初始化。

  • IntentService异步初始化
    其实也算是多线程的一种,IntentService是继承于Service并处理异步请求的一个类,在IntentService的内部,有一个工作线程来处理耗时操作,启动IntentService的方式和启动传统Service一样,同时,当任务执行完后,IntentService会自动停止,而不需要去手动控制。
public class InitIntentService extends IntentService {

    public static final String ACTION = "com.crazymo.performance.upgrade";

    public InitIntentService() {
        super("InitIntentService");
    }

    public static void start(Context context) {
        Intent intent = new Intent(context, InitIntentService.class);
        intent.setAction(ACTION);
        context.startService(intent);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        //处理耗时操作
    }
}

将耗时任务丢到IntentService中去处理,系统会自动开启线程去处理,同时,在任务结束后,还能自己结束Service,最后只需要在Application或者Activity的onCreate中去启动这个IntentService即可,最后别忘了在清单中声明Service。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    InitIntentService.start(this);
}
  • 使用ActivityLifecycleCallbacks监控Activity的生命周期
    监控到所有Activity的生命周期,在这里,我们就可以通过onActivityCreated这样一个回调,来将一些UI相关的初始化操作放到这里,同时,通过unregisterActivityLifecycleCallbacks来避免重复的初始化。同时,这里onActivityCreated回调的参数Bundle,可以用来区别是否是被系统所回收的Activity。
public class MainApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化基本内容
        // ……
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                unregisterActivityLifecycleCallbacks(this);
                // 初始化UI相关的内容
                // ……
            }

            @Override
            public void onActivityStarted(Activity activity) {
            }

            @Override
            public void onActivityResumed(Activity activity) {
            }

            @Override
            public void onActivityPaused(Activity activity) {
            }

            @Override
            public void onActivityStopped(Activity activity) {
            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
            }

            @Override
            public void onActivityDestroyed(Activity activity) {
            }
        });
    }
}

4、资源优化

优化布局、布局层级,一个是优化资源,尽可能的精简资源、避免垃圾资源,经过混淆、使用webp或者其他工具尽量优化资源等等

PS

从早上11点开始一直到晚上1点终于把APP黑白屏以及APP启动背后的原理流程基本理清,这里插一句有些人说黑白屏机制可以避免,个人觉得这是不能避免的,所谓的避免仅仅是障眼法,因为从向系统申请内存到系统准备好内存从宏观角度上来说是需要消耗一定的时间的,但是如果某个ROM进行了优化能使这个时间在一个很小的阈值范围内,人眼都无法察觉,那可以说是避免了,至于文章内跨进程通信(在这一部分主要体现在于怎么从调用这个类的方法缺可以突然回调到另一个类的方法里)部分会留到后面分析Binder机制时专门讲解,最大的一个感受就是世上无难事只怕有心人,沉下心慢慢啃总有一天你会肯清楚明白的,书山有路勤为径,学海无涯乐作舟,共勉!!

猜你喜欢

转载自blog.csdn.net/crazymo_/article/details/80035314