Android 应用多角度启动优化

目录

一、介绍

1.1、冷启动和热启动

1.2、启动速度测量

1.2.1、TraceCompat

二、优化方式 

2.1、MultiDex启动优化

2、 子进程中执行dexOpt

2.2、启动窗口优化(设置主题)

2.3、Allpication,ContentProvider优化

2.4、Activity优化

2.4.1、Activity View的加载流程

2.4.2、懒加载IdleHandler

2.4.3、异步加载布局AsyncLayoutInflater

​2.5、Fragement优化

2.6、布局优化

2.6.1、诊断过度绘制,优化过度绘制

2.6.2、合理使用控件Constaintlayout

ViewStub

2.6.3、去掉window的默认背景

3.6.4、去掉其他不必要的背景有时候为了方便会先给Layout设置一个整体的背景,再给子View设置背景,这里也会造成重叠,如果子View宽度mach_parent,可以看到完全覆盖了Layout的一部分,这里就可以通过分别设置背景来减少重绘。再比如如果采用的是selector的背景,将normal状态的color设置为“@android:color/transparent",也同样可以解决问题。这里只简单举两个例子,我们在开发过程中的一些习惯性思维定式会带来不经意的Overdraw,所以开发过程中我们为某个View或者ViewGroup设置背景的时候,先思考下是否真的有必要,或者思考下这个背景能不能分段设置在子View上,而不是图方便直接设置在根View上。

3.6.5、ClipRect


一、介绍

1.1、冷启动和热启动

  • 冷启动当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,然后再根据启动的参数,启动对应的进程组件,这个启动方式就是冷启动
  • 热启动当启动应用时,后台已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动对应的进程组件,这个方式叫热启动

1.2、启动速度测量

1.2.1、TraceCompat

  • TraceCompat.beginSection(“xxx”);// 手动埋点起始点
  • TraceCompat.endSection();// 手动埋点结束点
  • python systrace.py -b 32768 -t 5 -a packageName -o trace.html sched gfx view wm am app
  • 使用python命令生成报告
  • cd到Android/sdk/platform-tools/systrace
  • 执行python命令,python环境没有配置好的话可使用Home Brew install一下
  • python systrace.py -b 32768 -t 10 -a 包名 -o browser.html sched gfx view wm am app
  • adb shell am start -w 包名/类名

 

返回的结果,就是标准的应用程序的启动时间

  • ThisTime:最后一个Activity启动耗时;
  • TotalTime:启动时经历的所有Activity启动耗时;
  • WaiteTime:AMS启动所有Activity的总时间

 

http://static.open-open.com/lib/uploadImg/20151231/20151231215915_677.png

  • startTime记录的刚准备调用startActivityAndWait()的时间点
  • endTime记录的是startActivityAndWait()函数调用返回的时间点
  • WaitTime = startActivityAndWait()调用耗时。

二、优化方式 

应用进程不存在的情况下,从点击桌面应用图标,到应用启动(冷启动),会经历以下流程:

Android 应用多角度启动优化_第1张图片

  1. Launcher startActivity
  2. AMS startActivity
  3. Zygote fork 进程
  4. ActivityThread main()
  5. ActivityThread.attach
  6. handleBindApplication
  7. Application.attachBaseContext
  8. ContentProvider.installContentProviders
  9. Application.onCreate
  10. ActivityThread 进入loop循环
  11. Activity生命周期回调,onCreate、onStart、onResume...

2.1、MultiDex启动优化

目前 Android 5.0 以上的设备已经自身支持了 MultiDex 功能,也就是说在安装 apk 的时候,系统已经会帮我们把 apk 里面的所有 dex 文件都做好 Optimize 处理,所以不需要我们在代码里启用 MultiDex 了。但是对于 Android 5.0 以下的设备,依然要求我们启用 MultiDex。而这些系统的设备在第一次运行 App 的时候,需要对所有的 Secondary Dexes 文件都进行一次解压以及 Optimize 处理(生成 odex 文件),这段时间会有明显的耗时,所有会产生明显的卡顿现象。

Android 应用多角度启动优化_第2张图片

配置:

android {
    defaultConfig {
        ...
	multiDexEnabled true // Enable MultiDex.
	//定义main dex中必须保留的类
multiDexKeepProguard file('mainDexClasses.pro')
        ...
    }
    ...
}
dependencies {
  implementation 'androidx.multidex:multidex:2.0.0'
}

Android 应用多角度启动优化_第3张图片

1、在Application的attachBaseContext启动新进程执行dexOpt

protected void attachBaseContext(Context base) {
    // 只有5.0以下需要执行 MultiDex.install
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
        MULTI_DEX = MULTI_DEX + "_" + getVersionCode(base);
        if (SystemUtil.isInMainProcess(base)) {
            // 判断有没有执行过dexOpt
            if (!dexOptDone(base)) {
                preLoadDex(base);
            }
        }
        if (!KwaiApp.isMultiDeXProcess(base)) {
            MultiDex.install(base);
        }
    }
    super.attachBaseContext(base);
}

/**
   * 是否进行过DexOpt操作。
   * 
   * @param context
   * @return
   */
private boolean dexOptDone(Context context) {
    SharedPreferences sp = context.getSharedPreferences(MULTI_DEX, MODE_MULTI_PROCESS);
    return sp.getBoolean(MULTI_DEX, false);
}

/**
   * 在单独进程中提前进行DexOpt的优化操作;主进程进入等待状态。
   *
   * @param base
   */
public void preLoadDex(Context base) {
    // 在新进程中启动PreLoadDexActivity
    Intent intent = new Intent(base, PreLoadDexActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    base.startActivity(intent);
    while (!dexOptDone(base)) {
        try {
            // 主线程开始等待;直到优化进程完成了DexOpt操作。
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2、 子进程中执行dexOpt

public class PreLoadDexActivity extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    super.onCreate(savedInstanceState);
    // 取消掉系统默认的动画。
    overridePendingTransition(0, 0);
    setContentView(R.layout.tv_splash_layout);
    new Thread() {
      @Override
      public void run() {
        try {
          // 在子线程中调用
          MultiDex.install(getApplication());
          SharedPreferences sp = getSharedPreferences(App.MULTI_DEX, MODE_MULTI_PROCESS);
          sp.edit().putBoolean(App.MULTI_DEX, true).commit();
          finish();
        } catch (Exception e) {
          finish();
        }
      }
    }.start();
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    System.exit(0);
  }
}

2.2、启动窗口优化(设置主题)

在等待第一帧显示的时间里,可以加入一些配置以增加体验,比如加入Activity的background,这个背景会在显示第一帧前提前显示在界面上。

1、android:theme配置

Android 应用多角度启动优化_第4张图片

 

2.、在MainActivity中加载布局前把AppTheme重新设置给MainActivity:

(1)设置背景图Theme
通过设置一张背景图。 当程序启动时,首先显示这张背景图,避免出现黑屏

(2)设置透明Theme
通过把样式设置为透明,程序启动后不会黑屏而是整个透明了,等到界面初始化完才一次性显示出来

两者对比:
Theme1 程序启动快,界面先显示背景图,然后再刷新其他界面控件。给人刷新不同步感觉。
Theme2 给人程序启动慢感觉,界面一次性刷出来,刷新同步。

2.3、Allpication,ContentProvider优化

Android 应用多角度启动优化_第5张图片

Application生命周期中也不要启动其他的组件如:service、contentProvider、

BroadcastReceiver.

StartUp:https://www.pianshen.com/article/10031906037/

StartUp 通过ContentProviderAndroid四大组件之一,这个组件相对来说是比较重量级的。也就是说,本来我的初始化操作可能是一个非常轻量级的操作,依赖于ContentProvider之后就变成了一个重量级的操作了代码中定义了一个LibraryA类并且实现了Initializer< Dependency >接口。这个时候我们看到dependencies()方法返回的就不是空列表了,而是包含了LibraryA的一个列表,这样LibraryA要想初始化,必须先初始化Dependency

Android 应用多角度启动优化_第6张图片

Android 应用多角度启动优化_第7张图片

假设你的应用程序依赖了LibraryA,并且需要在程序一开始启动时就初始化LibraryA,定义一个LibraryA类并且实现Initializer接口:

Android 应用多角度启动优化_第8张图片

Android 应用多角度启动优化_第9张图片

代码中定义了一个LibraryA类并且实现了Initializer< Dependency >接口。这个时候我们看到dependencies()方法返回的就不是空列表了,而是包含了LibraryA的一个列表,这样LibraryA要想初始化,必须先初始化Dependency

自动启动:

tools:node="merge"属性是为了确保清单合并工具可能造成的冲突问题

手动:

Android 应用多角度启动优化_第10张图片

禁用自动初始化后,你可以使用AppInitializer手动初始化组件和它的依赖。还是看下官方代码:

2.4、Activity优化

Activity是 用户操作的可视化界面;它为用户提供了一个放置视图和交互操作的窗口。采用setContentView的方法提供。因此,可以理解Activity、Window、View三者关系为。Activity提供Window ,View被添加到Window中。

 

2.4.1、Activity View的加载流程

1、Activity在被创建之初,调用了attach方法,这个时候,为Activity创建了一个PhoneWindow, 并且为PhoneWindow设置了事件交互的回调。

2、紧接着Activity的onCreate()方法被回掉。这里也就到了我们经常复写方法,我们在OnCreate()之中,调用setContentView(id)。

3、在setContentView(), PhoneWindow 创建了一个顶级视图 DecorView (FrameLayout)的子类。

4、紧接着,DecorView会依据一些feature(类似NO_ACTICON_BAR)来,添加一个layout。这个Layout中包含了Title、content。其中content也是FrameLayout,也就是我们在setContentView(id),将视图添加的父容器。

Android 应用多角度启动优化_第11张图片

Android 应用多角度启动优化_第12张图片

在应用启动的时候,为了加快启动速度,往往需要把一些比较重的操作放到子线程中,或者是延时加载。将任务放在子线程中是一个比较简单并且看起来有效的操作,但是呢,也不能太过于依赖子线程,它虽然不会阻塞主线程,但是却会跟主线程抢占CPU,当子线程很多并且任务很重的时候,也还是会拖慢主线程的。

2.4.2、懒加载IdleHandler

以前一直在想Android为什么不在Activity或者Fragment中提供一个接口,让我们可以在主线程空闲的时候去执行一些操作,后来发现真的有,但这个接口不是在Activity和Fragment中,而是在MessageQueue中,在MessageQuque

public staticinterface IdleHandler{
    boolean 
    queueIdle();
}

Android 应用多角度启动优化_第13张图片

简单来说,就是当MessageQueue中没有更多的消息的时候就会回调queueIdle()这个方法,返回true的话,当MessageQueue中没有消息的时候还会继续回调这个方法,返回false则会在执行完之后移除掉这个监听。

原理就是这么简单了,接下来就是动手优化代码了,代码也很简单。

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

 
    // ...

 
    // 
拿到主线程的MessageQueue
    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {

 
        @Override
        public boolean queueIdle() {
            //  
在这里去处理你想延时加载的东西
            delayLoad();

 
            // 
最后返回false,后续不用再监听了。
            return false;
        }
    });
}

 

2.4.3、异步加载布局AsyncLayoutInflater

new AsyncLayoutInflater(this).inflate(R.layout.launcher,null, new AsyncLayoutInflater.OnInflateFinishedListener(){
       @Override
       public void onInflateFinished(View view, int resid, ViewGroup parent) {
           setContentView(view);
       }
   });

Android 应用多角度启动优化_第14张图片
2.5、Fragement优化

https://www.jianshu.com/p/2201a107d5b5?utm_campaign=hugo

在没有添加懒加载之前,只要使用 add+show+hide 的方式控制并显示 Fragment, 那么不管 Fragment 是否嵌套,在初始化后,如果只调用了add+show,同级下的 Fragment 的相关生命周期函数都会被调用。且调用的生命周期函数如下所示:

onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume

Fragment 完整生命周期:onAttach -> onCreate -> onCreatedView -> onActivityCreated -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy -> onDetach

什么是同级 Frament 呢?看下图

 

https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8xMjE2OTA4OS0xMzQ1NmQ4NWI2MDRmNGYyLnBuZw?x-oss-process=image/format,png

上图中,都是使用 add+show+hide 的方式控制 Fragment,

在上图两种模式中:

  • Fragment_1、Fragment_2、Fragment_3 属于同级 Fragment
  • Fragment_a、Fragment_b、Fragment_c 属于同级 Fragment
  • Fragment_d、Fragment_e、Fragment_f 属于同级 Fragment

观察上图我们可以发现,同级的Fragment_1、Fragment_2、Fragment_3 都调用了 onAttach...onResume 系列方法,也就是说,如果我们没有对 Fragment 进行懒加载处理,那么我们就会无缘无故的加载一些并不可见的 Fragment , 也就会造成用户流量的无故消耗(我们会在 Fragment 相关生命周期函数中,请求网络或其他数据操作)。

在Fragment变为可见时都会执行onResume()方法,我们可以利用这一点来实现懒加载,基本思路有两点:

  • 将Fragment加载数据的逻辑放到onResume()方法中,这样就保证了Fragment可见时才会加载数据。
  • 声明一个变量标记是否是首次执行onResume()方法,因为每次Fragment由不可见变为可见都会执行onResume()方法,需要防止数据的重复加载。此外,如果我们使用的是FragmentPagerAdapter,切换导致Fragment被销毁时是不会执行onDestory()和onDetach()方法的,只会执行到onDestroyView()方法,因此在onDestroyView()方法中我们还需要将这个变量重置,否则当Fragment再次可见时就不会重新加载数据了。
abstract class LazyFragment : Fragment() {

    /**
     * 是否执行懒加载
     */
    private var isLoaded = false

    /**
     * 当前Fragment是否对用户可见
     */
    private var isVisibleToUser = false

    /**
	* 当使用ViewPager+Fragment形式会调用该方法时,setUserVisibleHint会优先Fragment生命周期函数调用,
     * 所以这个时候就,会导致在setUserVisibleHint方法执行时就执行了懒加载,
     * 而不是在onResume方法实际调用的时候执行懒加载。所以需要这个变量
     */
    private var isCallResume = false

 /**
     * 是否调用了setUserVisibleHint方法。处理show+add+hide模式下,默认可见 Fragment 不调用
     * onHiddenChanged 方法,进而不执行懒加载方法的问题。
     */
    private var isCallUserVisibleHint = false

    override fun onResume() {
        super.onResume()
       isCallResume = true
        if (!isCallUserVisibleHint) isVisibleToUser = !isHidden
        judgeLazyInit()
    }


    private fun judgeLazyInit() {
        if (!isLoaded && isVisibleToUser && isCallResume) {
            lazyInit()
            Log.d(TAG, "lazyInit:!!!!!!!”)
            isLoaded = true
        }
    }

    override fun onHiddenChanged(hidden: Boolean) {
        super.onHiddenChanged(hidden)
        isVisibleToUser = !hidden
        judgeLazyInit()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        isLoaded = false
        isVisibleToUser = false
        isCallUserVisibleHint = false
        isCallResume = false
    }

    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        super.setUserVisibleHint(isVisibleToUser)
        this.isVisibleToUser = isVisibleToUser
        isCallUserVisibleHint = true
        judgeLazyInit()
    }

    abstract fun lazyInit()
}
  1. 将需要显示的 Fragment ,在调用 add 或 show 方法后,setMaxLifecycle(showFragment, Lifecycle.State.RESUMED)
  2. 将需要隐藏的 Fragment ,在调用 hide 方法后,setMaxLifecycle(fragment, Lifecycle.State.STARTED)

2.6、布局优化

2.6.1、诊断过度绘制,优化过度绘制

过度绘制:屏幕上某一像素点在一帧中被重复绘制多次

  • 分类(根据层度):
  • 无过度绘制(一个像素只被绘制了一次)      (原色)
  • 过度绘制x1(一个像素被绘制了两次)        (蓝色)
  • 过度绘制x2(一个像素被绘制了三次)        (绿色)
  • 过度绘制x3(一个像素被绘制了四次)        (粉色)
  • 过度绘制x4+(一个像素被绘制了五次以上)   (红色)
Android调试机中的开发者选项中开启【调试GPU过度绘制】

Android 应用多角度启动优化_第15张图片

 

2.6.2、合理使用控件Constaintlayout

https://developer.android.google.cn/reference/android/support/constraint/ConstraintLayout

用Constaintlayout的话最多就两个层级了,不像Relative和Linear一样一层嵌套一层的。

1.尽量多使用 ConstraintLayout、RelativeLayout、LinearLayout
2.尽量使用 ConstraintLayout
3.在布局的层级相同的情况下,使用 LinearLayout 代替 RelativeLayout
4.在布局复杂或者层级过深的时候,使用 RelativeLayout 代替 LinearLayout 使界面层级扁平化,降低层级

 

使用include 和merge标签减少复用布局而产生的布局嵌套,使用ViewStub懒加载减少渲染元素

ViewStub

  • 优点:它是一个轻量级的View,是一个看不见的,不占布局位置,占用资源非常小的控件。我们可以在ViewStub下指定要加载的布局并指定布局id,当我们需要该布局显示的时候,只需要调用ViewStub的inflate()即可。或者setVisibility();
  • 缺点:ViewStub的inflate()只能调用一次,多次调用会有异常抛出。也就是说我们只能对ViewStub加载的布局控制一次
  • 1.将可重复使用的布局通过include标签与merge标签搭配进行使用
    2.尽量少添加不必要的背景,减少过度绘制
    3.布局能扁平化的扁平化,尽量不要增加布局层级
    4.适当的时侯某些控件使用懒加载ViewStub
    5.使用Hierarchy View 工具或者Lint工具来进行app的检测 
    

2.6.3、去掉window的默认背景

当我们使用了Android自带的一些主题时,window会被默认添加一个纯色的背景,这个背景是被DecorView持有的。当我们的自定义布局时又添加了一张背景图或者设置背景色,那么DecorView的background此时对我们来说是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。去掉window的背景可以在onCreate()中setContentView()之后调用
getWindow().setBackgroundDrawable(null);

或者在theme中添加
android:windowbackground="null";

3.6.4、去掉其他不必要的背景
有时候为了方便会先给Layout设置一个整体的背景,再给子View设置背景,这里也会造成重叠,如果子View宽度mach_parent,可以看到完全覆盖了Layout的一部分,这里就可以通过分别设置背景来减少重绘。再比如如果采用的是selector的背景,将normal状态的color设置为“@android:color/transparent",也同样可以解决问题。这里只简单举两个例子,我们在开发过程中的一些习惯性思维定式会带来不经意的Overdraw,所以开发过程中我们为某个View或者ViewGroup设置背景的时候,先思考下是否真的有必要,或者思考下这个背景能不能分段设置在子View上,而不是图方便直接设置在根View上。

3.6.5、ClipRect

为了解决Overdraw的问题,Android系统会通过避免绘制那些完全不可见的组件来尽量减少消耗。但是不幸的是,对于那些过于复杂的自定义的View(通常重写了onDraw方法),Android系统无法检测在onDraw里面具体会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。除了clipRect方法之外,我们还可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。

 

你可能感兴趣的:(总结,启动优化,性能优化)