Android进阶课学习收获(27~28)

17 篇文章 1 订阅
10 篇文章 1 订阅

第27讲:面对UI卡顿,如何入手分析解决问题?

       对于UI性能分析,Systrace是目前使用最广的工具。它能够帮助开发者分析多个模块的运行状态以及详细信息。比如SurfaceFlinger、View刷机机制等。通过Android提供的脚本systrace.py,可以设置数据采集方式并收集相关程序运行数据,最终生成一个网页文件,提供程序开发者分析程序性能问题。

Systrace简单使用

在Android SDK中提供了运行Systrace的脚本,具体路径在 android-sdk/platform-tools/systrace/文件夹中。在此目录下执行以下命令,就可以抓取Systrace日志:

python systrace.py --time=10 -o my_systrace.html

Systrace生成的是html文件,需要使用Chrome打开my_systrace.html,显示效果如下:

但Systrace的使用有一定的难度,技术门槛略高。了解屏幕的刷新机制能够更好地理解Systrace中各个部分代表的含义。

CPU & GPU

       我们知道View的绘制会经过Measure、Layout、Draw这3个阶段,而这三个阶段的工作都是由CPU来负责完成。另外CPU还会负责一些用户输入、View动画等事件,这部分工作都是在UI线程中完成。

       当CPU绘制完成之后,会在RederThread线程中将这部分数据提交给GPU。GPU负责对这些数据进行栅格化操作,并将数据缓存到一个Buffer中。

        最后手机屏幕再从这个Buffer中读取数据显示到屏幕上。实际上真正对Buffer中的数据进行合成显示到屏幕上的是SurfaceFlinger。具体流程如下:

     这个过程中有一个阶段是互相矛盾的,如果GPU正在想Buffer中做缓存,而此时屏幕也正好从Buffer中取数据,这是就可能造成Buffer中数据污染,屏幕有可能显示错乱。

     为了避免这种情况发生,Android又引入了双缓冲机制,意思就是有2个Buffer:Back Buffer和Frame Buffer。CPU提交的数据被缓存到Back Buffer中,然后GPU对Back Buffer中的数据做栅格化操作,完成之后将其交换(swap)到Frame Buffer中,最后屏幕从Frame Buffer中取数据显示。如下所示:

注意:因为GPU合成的数据会经常更新,所以它会负责定期的交换Back Buffer 和Frame Buffer的数据,从而保证屏幕上显示最新内容。如果CPU正在向Back Buffer中写入数据时,GPU会将Back Buffer锁定,如果此时正好到了交换两个Buffer的时间点,那么这次swap会被忽略放弃。直接导致的结果就是Frame Buffer中还保存着上一帧的数据,最终屏幕上也显示之前的内容,这也就是我们常说的丢帧因此为了保证APP能够流畅工作,我们需要在每帧16ms以内处理完所有的CPU与GPU的计算、绘制、渲染等操作。完美的屏幕绘制过程应当如下图所示:

Vsync

       通过双缓冲技术再加上应用层的优化,大多数情况下已经完全能满足程序流程运行。但是有时候还是会发生”丢帧“现象。这是因为屏幕刷新率和GPU绘制帧率并不一定是一致的。

        屏幕刷新率(screen refresh rate) :指手机屏幕每秒钟可以刷新多少次,目前在大多数厂商手机上的屏幕刷新率是60HZ,也就是以16.6ms进行一次刷新。GPU绘制率(frame rate):指的是GPU每秒能够合成多少帧。屏幕刷新率是一个硬件指标,软件层并没有办法对其修改,可以软件层触发View绘制的时机是随机的(代码里可以任意时间调用Invalidate或者requestLayout刷新),因此我们无法控制View绘制的起始时间。比如之前图中的绘制过程也有可能发生以下情况:

       可以看出在”帧1“阶段,虽然CPU和GPU所消耗时间小于16ms,但是他们开始的时间太晚,距离下一次屏幕刷新太近。所以当下一次屏幕刷新时,屏幕从Frame Buffer中拿到的数据还是”帧1“的数据,还是会”丢帧“。

      为了解决这个问题,Android系统引入了Vsync机制。每隔16ms硬件发出Vsync信号,应用层接收到此消息后会触发UI的渲染流程,同时Vsync信号也会触发SurfaceFlinger读取Buffer中的数据,进行合成显示到屏幕上。简单讲就是将上面的CPU和GPU的开始时间与屏幕刷新强行拖拽到同一起跑线。实现下图效果:

 

       可以看出Vsync的频率同屏幕刷新频率是一致的,因此View的渲染和SurfaceFlinger的合成也都按照Vsync的信号频率有条不紊的进行着。

Choreographer 编舞者

那么软件层是如何接收硬件发出的Vsync信号,并进行View绘制操作呢?答案就是choreographer。

何时注册Vsync信号

       Choreographer的postCallbackDelayedInternal方法最终调用了一个native的本地方法nativeScheduleVsync,这个方法实际上就是向系统订阅一次Vsync信号。Android系统每隔16ms会发送一个Vsync信号,但并不是所有APP都能收到的,只有订阅的才能收到。这样设计的合理之处在于,当UI没有变化的时候就不会去调用nativeScheduleVsync去订阅,也就不会收到Vsync信号,减少了不必要的绘制操作。

        每次订阅只能收到一次Vsync信号,如果需要收到下次信号需要重新订阅。比如Animation的实现就是在订阅一次信号之后,紧接着再次调用nativeScheduleVsync方法订阅下次Vsync信号,因此会不断地刷新UI。

何时接收Vsync信号

      注册Vsync信号的操作是由FrameDisplayEventReceiver中的nativeScheduleVsync方法实现的,而Vsync信号实际上也是由FrameDisplayEventReceiver来接收。当它接收到Vsync信号时,会调用其内部的onVsync方法。最终调用了Choreographer的doFrame方法,改方法非常重要,就是在这个方法中将之前插入到CallbackQueue中的Runnable取出来执行。其实Android屏幕绘制一帧主要就是处理Input、animation、traversal这3件事。

     Chroreographer是一个承上启下的角色。承上:接收应用层各种callback输入,包括input、animation、traversal绘制。但是这些callback并不会被立即执行。而是会缓存在Choreographer中的CallbackQueue中;启下:内部的FrameDisplayEventReceiver负责接收硬件层发成的Vsync信号。当接收到Vsync信号之后,会调用onVsync方法 --> doFrame  ---> doCallbacks,在doCallbacks中从CallbackQueue中取出进行绘制的TraversalRunnable,并调用其run方法进行绘制。

   通过这套机制,保证软件层和屏幕刷新处于同一频率,使Android APP可以以一个比较稳定的帧率运行,减少因为频率波动造成丢帧的概率。

   除了animation造成卡顿之外,还有以下几种常见的卡顿现象:自定义View的draw方法耗时太长;自定义view的measure和layout方法耗时太长;decodeBitmap转换图片耗时太长。

 总结:本节从Systrace的使用开始,要彻底掌握Systrace的使用,就必须理解系统中屏幕是如何刷新的。

  • 首先我们了解CPU和GPU是如何协同工作,将View绘制的数据保存在缓存Buffer中;
  • 然后为了解决Buffer的读取并发问题,Android引入了双缓冲机制;
  • 但是双缓冲机制也并不是完美的,因为软件层刷新时随机的,为此Android又引入了Vsync机制,Vsync机制的实现主要依赖Choreographer来实现。了解这整个过程后,再通过Systrace日志,就可以分析出具体是那一部分耗费性能较高,并针对性的做出优化。

XML布局显示到屏幕上行的流程

CPU将UI对象转为多维图形  --->    通过OpenGL调用GPU  ----->    GPU对图形进行栅格化  ---->显示器直接显示。

60Hz刷新频率由来

Android系统每隔16ms发出一个Vsync信号,触发对UI进行渲染,如果每次渲染都能成功,就能达到流畅的画面所需要的60fps,这就意味着,计算渲染的大多数操作都必须在16ms内完成。

如何减少时间

一、CPU减少xml转换成对象的时间:xml中减少布局层级;避免背景被重复绘制

二、GPU减少重复绘制;

优化是一种思想,而不是一套具体的方法,做UI优化的时候,可以从以下几个方面入手:

  • 布局中的背景是否有必要;
  • 自定义View是否进行了裁剪;
  • 布局能否做到扁平化(是否可以删除多余布局);
  • 当前页面是否存在非常耗内存的代码;
  • WebView是否通过单开进程方式;
  • 关于图片的计算是否需要动用代码;

内存抖动为什么会对UI产生卡顿

      对象在堆中创建的时候,会先从年轻代的Eden区查找是否有内存空间,如果Eden区没有,则看下年轻代里s0、s1区是否有内存空间,如果有,则将Eden区里对象赶进s区,在Eden取给新对象分配空间,如果s区也没有空间,则去查看老年代区。一般新new的对象都是在Eden区域分配内存空间,除非new的对象比较大,直接超过了Eden区域的大小,这种大对象会直接被放在老年代。

     内存的回收过程中,在做对象标记、筛选回收的时候,有可能暂停一会儿UI线程,所以说内存抖动可能产生UI卡顿。

假设ViewPager里有20个fragment,如何优化

优化的方案一定是扣空间或者时间。首先将每个fragment优化到极致(数据能不能少加载点?流程能不能优化?视觉能不能优化?);20个fragment能否少加载下?流程是否可优化?;

第28讲:Android Gradle 构建问题解析

          Android使用Gradle的门槛不算太高,只需要在Android Studio默认生成的build.gradle中稍加修改,都能满足要求,但深入细致的了解gradle的基本知识,还是能够帮助我们更优雅地实现项目的配置工作,有些场景gradle甚至能帮助我们完成一些业务上的需要。

gradle Task

Task可以理解为gradle的执行单元,gradle通过执行一个个Task来完成整个项目构建工作。

自定义Task

task A{
    println 'this is task A'
}

上述定义了一个简单的Task A,然后在终端输入以下命令执行此Task,可以看到打印结果,如果gradle未安装,可以先在命令行执行 brew  install gradle  进行安装。结果如下:

从结果可以看出,打印日志是在gradle的配置(Configure)阶段执行的。gradle的构建生命周期包含3部分:初始化阶段、配置阶段、执行阶段。在task A中添加doFirst 闭包,如下所示:

task A{
    println 'this is task A'

    doFirst{
        println 'this is in execute'
    }
}

再次执行gradle A,打印如下:

gradle在运行期会执行所有task的配置语句,然后执行执行的task。

Task之间可以存在依赖关系

task A{
    println 'this is task A'
    doFirst{
        println 'task A is executing'
    }
}

task B{
    dependsOn 'A'
    println 'this is task B'
    doFirst{
        println 'task B is executing'
    }
}

在命令行里执行”gradle B“,执行结果如下:

gradle会在配置Configure阶段,确定依赖关系。对于Android来说即为执行各个module下的build.gradle文件,这样各个build.gradle文件中的task的依赖关系就被确认下来了,而这个依赖关系的确定就是在Configuration阶段。

gradle自定义方法

我们可以在build.gradle 中使用def关键字,自定义方法,比如如下代码中自定义了getDate方法,并在task中使用:

def getDate() {
    def sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.CHINA);
    def nowDateStr = "\" + sdf.format(new Date()) + "\"
    return nowDateStr;
}

task my_task{
    doFirst{
        println 'today is ' + getDate()
    }
}

系统预置task

自定义task时,还可以使用系统提供的各种显示task来完成相应的任务。具体就是使用关键字type来指定使用的是哪一个task。我们在项目下建立两个文件夹dst和src并在src下添加一个Demo.java文件,我们可以在build.gradle中添加一个task如下:

task copy(type: Copy){
    from 'src'
    into 'det'
}

在命令行中执行”gradle copy“,运行结束后命令行输入tree,查看当前目录结构可知,已经拷贝到dst目录下一个Demo.java文件。

除了Copy之外,还有很多其他显示的task可用,比如我们可以通过自定义task实现编译Demo.java并将编译后的.class输出到某一特定路径,具体实现如下:

task compile(type: JavaCompile){
    source = './src/'
    include 'Demo.java'
    classpath = files('.')
    destinationDir = file('./build')
}

执行命令发现,在build目录下多了一个Demo.class文件。

gradle project

在Android中每个module就对应这一个project,gradle在编译期间会为每个project创建一个Project对象用来构建项目,这一过程是在初始化阶段,通过解析setting.gradle中的配置来创建相应的Project。

上图setting.gradle中导入了3个project,但是实际上还会有一个根project,使用 ./gradlew project查看,如下所示:

我们可以在根project中统筹管理所有的子project,具体在build.gradle中进行配置,如下所示:

//为app module 进行设置
project('app') { Project project ->
    //比如为App工程强制使用某个版本的依赖来解决依赖冲突中出现的依赖
    project.configurations.all {
        resolutionsStrategy{
            force 'com.android.support:support-annitations:26.1.0'
        }
    }
    apply plugin:'com.android.application'
    //指定版本
    version '1.0.0'
    //凡是project中可以配置的都可以进行配置
    //比如添加依赖
    dependencies{
    }
    //添加android相关配置
    android{
        ...
    }
}
//设置core module
project('core') { Project project ->
    apply plugin:'com.android.library'
    
    dependencies{
        ...
    }
}
//设置library module
project('library') { Project project ->
    apply plugin: 'com.android.library'
    dependencies{
        ...
    }
}

这样写的好处是项目中所有module的配置都统一写在一个地方,统筹管理。比如经常会在主项目的build.gradle中添加包过滤,解决依赖冲突,如下所示:

subprojects{
    project.configurations.eachDependency{ detail ->
        if(details.requested.group == 'com.android.support'
            && !details.requested.name.contains('multidex')){
            //改这个版本号到你想要的版本
            details.useVersion "${cfgs.androidSupportVersion}" 
        }
        
        if(details.requested.group == 'com.android.support'
            && details.requested.name.contains('multidex')){
            //改这个版本号到你想要的版本
            details.useVersion "${cfgs.multidex}" 
        }
        if(details.requested.group == 'io.reativex.rxjava2'){
            if(details.requested.name.contains('rxjava')){
                //改这个版本号到你想要的版本
                details.useVersion "${cfgs.rxjava_version}"
            }
            if(details.requested.name.contains('rxandroid')){
                //改这个版本号到你想要的版本
                details.useVersion "${cfgs.rxandroid_version}"
            }
        }
    }
}

buildSrc统筹依赖管理

随着项目越来越大,工程中的module越来越多,依赖的三方库也越来越多。一般情况下我们会在一个集中的地方统一管理这些三方库的版本。比如像谷歌官方推荐使用ext变量,在根module的build.gradle中使用ext集中声明各种三方库的版本,如下所示:

ext{
    //SDK and tools
    minSdkVersion = 21
    targetSdkVersion = 28
    compileSdkVersion = 28
    buildToosVersion = '28.0.3'

    //APP dependencies
    supportLibraryVersion = '28.0.0'
    guavaVersion = '18.0'
    ...
}

然后在子module中,引用这些版本信息。

dependencies{
    implementation "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"
"com.squareup.retrofit2:apapter-rxjava:$rootProject.retrofitVersion"
}

但这种写法有点小瑕疵:不支持AS的自动补充功能,也无法使用代码自动跟踪,因此可以考虑使用buildSrc。buildSrc是Android项目中一个比较特殊的project,在buildSrc中可以编写Groovy语言,但是现在谷歌也越来越推荐使用kotlin来编写语句。

先在根路径下创建目录buildSrc,这个工程必须只能有一个名字必须为buildSrc,结构如下:

创建好之后在buildSrc中创建build.gradle.kts文件,并添加kotlin插件。

//build.gradle.kts

import org.gradle.kotlin.dsl.`kotlin-dsl`

plugins {
    `kotlin-dsl`
}

编译工程有可能报错,如下所示:

只要添加repositories { jcenter() } 仓库即可。

接下来在buildSrc中创建src/main/java目录,并在此目录下创建Dependencies.kt(名字可随意取)。在Dependencies.kt中创建两个object,分别用来管理工程中的版本信息以及依赖库。

//统一管理项目中的版本信息
object Versions {

    //Build Config
    const val minSDK = 14
    const val compileSDK = 27
    const val targetSDK = 27

    //Kotlin
    const val Kotlin = "1.2.50"

}

//统一管理项目中使用的依赖库
object Deps{
    const val KotlinStdLib = "org.jetbrains.lotlin:kotlin-stdlib-jdk7:${Versions.support}"
}    

最后我们就可以直接在各个module中的build.gradle中直接使用Deps中的变量来声明依赖,比如在app module的build.gradle中添加如下依赖。

//使用buildSrc之前的使用
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

//使用buildSrc中的Deps添加依赖
implementation Deps.kotlinStdLib

上图分别是使用buildSrc前后的对比,并且在Deps的过程中,studio会给出自动提示。

总结

      本节课主要介绍了gradle构建中的task和project。

       task与开发者最为紧密,它是gradle构建的基本单元。我们每次编译工程的时,Android Studio会在控制台打印出执行的task的名称,类似下图中的格式。

我们也可以自定义task实现相同的构建需求。

      project对饮项目中的module,每个module中包含一个build.gradle,每个build.gradle都会被gradle编译成Project字节码。我们再build.gradle中所有逻辑,实际上最终都会被映射成此Project字节码内的实现逻辑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值