性能优化是一个app很重要的一部分,一个性能优良的app从被下载到启动到使用都能给用户到来很好的体验。自然我们做性能优化也是从被下载(安装包优化)、启动(启动优化)、使用
性能优化是一个app很重要的一部分,一个性能优良的app从被下载到启动到使用都能给用户到来很好的体验。自然我们做性能优化也是从被下载(安装包优化)、启动(启动优化)、使用(渲染优化、耗电优化、内存优化.........)等入手。因为我也是个菜鸟,所有东西都是现学的,所以过程中有任何问题都可以提出来,大家一起长知识。 安装包优化当今手机的内存普遍是128G或者256G,当用户长时间使用,产生了大量数据后,留给app安装的空间可能只有几十个G,甚至更少。所以一个app的大小可能就决定了用户是否选择你。 优化方案:
启动优化启动优化可以说是性能优化里很重要很重要的一个部分了,用户拿到你的app,第一印象自然是app启动的界面,app启动的流畅度和时间长短,可以说启动性能就是一个app的门面。(最讨厌app启动时候的广告了) 大家可能都听说过2-5-8原则:
所以不管你的app做的再怎么牛逼,用户点进你的app,反应速度让他很失望,用户也无继续使用的欲望。那么我们应该如何去规划整体的启动优化呢?具体方案如下: 冷启动、热启动和温启动什么是冷启动、热启动、温启动?
由此可见启动最慢的是冷启动,最快的是热启动。着重优化的地方也是冷启动。 在冷启动下会进行如下的相关流程 与我们代码相关的只有创建Application之后到首帧绘制之前。
Activity里面的优化和Application差不多,但是Activity.onCreate方法的开销是最大的,对整个app启动的影响也最大,所以绝对不能再里面执行太耗时的操作。其次是对布局优化也可以缩短onCreate的时间,具体见渲染优化。 这里再介绍几个用于检测app启动性能的工具:
内存优化在Android的虚拟机中,每fork一个进程,它的内存是给定的,因为移动设备的内存相对PC比较小,资源紧张,因此一个app在运行过程中一定要管理好自己的那部分内存,以提高稳定性。在内存使用中经常出现的问题也是内存抖动和内存泄漏了。 内存抖动内存抖动是由于短时间内有大量对象进出JVM的新生区导致的,内存忽高忽低,有短时间内上升和下落的趋势,分析图成锯齿状。 它伴随着频繁的GC(Garbage Collection垃圾回收),频繁GC会大量占用UI线程和CPU资源,会导致APP整体卡顿,甚至OOM。 先说为什么频繁GC会导致APP整体卡顿?在JVM的GC机制中,垃圾回收有单线程收集和多线程收集,但不管是哪种回收方式,在回收的时候所有用户线程都会被暂停(STW),具体原理涉及JVM的知识了,就不再深入了。所以频繁地GC,用户线程就会被频繁地暂停,自然app就会卡顿。 为什么频繁GC也有可能会OOM?先看一张图 这里简单说一下JVM的空间担保机制,简单理解就是Java堆划分为新生代 (Young Generation)和老年代(Old Generation)两个区域,新生代空间比较少,只有1/3,而老年代有2/3,新生代中不断有对象被创建然后回收,只有少部分仍然存在的对象会进入老年代。而当频繁GC时,会导致新生代中有大量对象被创建,然后新生代空间就会不够用,这时候老年代就会划分一部分空间用来给新生代创建大量的对象。这就是JVM的空间担保机制。但是当老年代被划出一部分空间后,假如这时候有一个比较大的对象,比如一张图片,从新生区转移到了老年区,但是这时候老年区被缩小了,剩下的空间不够了,这时候就触发了OOM。 怎么监测内存抖动?AS有自带的检测内存抖动的工具-----Memory Monitor 其实这个在启动优化工具里面也提到过。 打开方式:Profiler ->SESSIONS右边的加号选择你的手机在选择你的app 就会出现这样的界面 这次我们不点B,选择C区Memory,这时候就会出现如下界面 A依然是一些事件的反应,B是内存使用的图形化显示,C是鼠标放在图形上就会有各个语言占用内存情况,D是时间轴,但这是内存使用正常的情况,当出现频繁GC的情况时 是这样滴,底部还会有一排垃圾桶表示频繁回收。那么如何定位呢?我们看到左侧有三个选项:
这里一般发生内存抖动都是由于频繁创建java/kotlin对象引起的,所以我们选择第三个并点击Record,等待一会就会出现这样的界面 上面一排排的垃圾桶就表示在频繁GC,下面的表格显示了各个对象内存分配情况,我们点击最多的char数组 跟踪可以发现是stringPlus相关操作引起的GC频繁,再看String 这里就追踪到了,原来是MainActivity里面的manyGCTest方法的问题。再看源码 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.button).setOnClickListener { Thread{ while (true) manyGCTest() }.start() } } private fun manyGCTest() { var str = "" repeat(10000){ str += it } Thread.sleep(100) } ? } 复制代码 给一个按钮设置监听,按下开启线程,在一个死循环里面进行10000次字符串拼接操作,实际上每次str+=it都会创建一个对象然后进行字符串拼接,但如果我们换成这样,情况会有所好转 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.button).setOnClickListener { Thread{ while (true) manyGCTest() }.start() } } ? private fun manyGCTest() { /*var str = "" repeat(10000){ str += it }*/ val sb = StringBuilder() repeat(10000){ sb.append(it) } Thread.sleep(100) } ? } 复制代码 内存抖动减轻 这是因为StringBuilder做字符串拼接只会创建一次对象,所以我们在大量字符串拼接中能使用StringBuilder尽量使用StringBuilder。其实这样的情况也是比较常见的,比如在onDraw里面涉及了很多用Color.parseColor()来解析颜色,但是parseColor里面也涉及了很多字符串的操作,如果一个自定义View比较复杂这种操作很多的话这也会影响app的性能,再或者存储Cookie等等。具体的一些字符串拼接方式的区别这里也不多说了,给出一篇博客:七种java字符串拼接详解 - ```...简单点 - 博客园 (cnblogs.com) 内存泄漏原理内存泄漏可以说是面试必问的,也是我们开发者所必须熟知的。那么什么是内存泄漏呢?就是程序中已经动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。简单来说就是一个对象该被回收却没有被回收,造成了内存浪费。那我们怎么知道一个对象怎么才能被GC回收呢?看一张图 在JVM中判断一个对象是否应该被回收一般根据可达性分析,如果一个对象的根可达,那它就不应该被回收,反之应该被回收。那么什么是根呢?就是GC roots,GC roots 一般有静态变量,线程栈变量,常量池,JNI(指针)等。举个例子,在我们还没学架构之前一直用的MVC,即所有的网络相关的操作都在Activity中进行,然后用Handler进行线程切换。但是在Handler作为非静态内部类的时候是有可能发生内存泄漏的,因为非静态内部类Handler会持有外部Activity的引用,而message会持有Handler的引用(具体见Handler源码),message会被messageQueue引用,messageQueue又被Looper引用,Looper又被Threadlocal引用,而Threadlocal属于Thread的变量即线程栈变量(GC roots即变量的根)。如果此时message是个延迟消息,而恰好在这延迟的时间段里面Activity被销毁了但是因为它还在被message引用造成它有根,不能被及时回收而一直占用内存。比教好的方案是把Handle写成静态内部类,因为静态内部类是不会持有外部的引用的,或者在onDestroy里面移除所有message。 这里说个题外话,java的内存泄漏和C/C++有什么区别呢? 在java中,一个进程其实就是一个JVM的实例,进程中的操作都是靠JVM托管的。假如我开启了两个java进程A和B,A用来打游戏,B用来学高数。假如这时候我不想学习了,就是B发生了内存泄漏,B进程就挂掉了,但这并不影响A进程的进行,你挂你的,我运行我的。但在C/C++中就不一样了,C/C++中没有JVM,发生内存泄漏了影响的是整个操作系统,这个时候只有重启操作系统才会使被浪费的空间得到重用。这也就是为什么电脑用久了不重启一次就会变卡,而手机不会。 怎么检测内存泄漏呢?上面提及了一种方案,就是使用AS自带的Android Profiler工具再结合MAT分析,但这个做法比较低效,难度也比较大,而且如果app比较庞大容易卡死AS,现今比较常用的工具是LeakCanary,它的使用比较高效,方法也比较简单。其实LeakCanary也是基于MAT进行检测Android应用程序的开源工具。 具体使用:在你的App中加入如下依赖: debugImplementation 'com.squareup.leakcanary:leakcanary-android:x.x.x' 复制代码 然后在启动App的时候就额外出现一个金丝雀的图标 这是时候内存泄漏检测就开始了,在你操作App的时候,如果这时候发生了内存泄漏状态栏就会有通知,比如我的手机 点击通知它就会开始下载文件然后开始分析,分析完之后又会给你一个通知,此时再点进去就能看到LeakCanary为我们生成的发生内存泄漏对象的引用树 可以很明显看到是SecondActivity被MyThread引用而发生内存泄漏,此时再看源码的确如此 class SecondActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_second) MyThread().start() } inner class MyThread:Thread(){ override fun run() { sleep(6*6*1000) } } } 复制代码 我在进入SecondActivity的时候开启了一个线程并让这个线程睡眠36秒,这时候我再推出当前Activity它不内存泄漏才怪呢。 关于LeakCanary源码分析可以看看我的另外一篇文章,下面是一些常见的内存泄漏:
渲染优化在上一章我们说activity在onCreate的时候会绘制布局,这也是性能优化很重要的一个点。 通过学习view的绘制流程我们知道,对于屏幕刷新频率60hz的手机来说,如果在1000/60=16.67ms内没有把这一帧的任务执行完毕,就会发生丢帧的现象,丢帧是造成界面卡顿的直接原因,渲染操作通常依赖于两个核心组件:CPU与GPU。CPU负责包括Measure,Layout等计算操作,GPU负责Rasterization(栅格化)操作。 所谓栅格化,就是将矢量图形转换为位图的过程,手机上显示是按照一个个像素来显示的,比如将一个Button、TextView等组件拆分成一个个像素显示到手机屏幕上。而UI渲染优化的目的就是减轻CPU、GPU的压力,除去不必要的操作,保证每帧16ms以内处理完所有的CPU与GPU的计算、绘制、渲染等等操作,使UI顺滑、流畅的显示出来。 过度绘制UI渲染优化的第一步就是找到Overdraw(过度绘制),即描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在重叠的UI布局中,如果不可见的UI也在做绘制的操作或者后一个控件将前一个控件遮挡,会导致某些像素区域被绘制了多次,从而增加了CPU、GPU的压力。 那么如何找出布局中Overdraw的地方呢?很简单,就是打开手机里开发者选项,然后将调试GPU过度绘制的开关打开即可,然后就可以看到应用的布局是否被Overdraw,比如我打开了调试过度绘制的开关,然后看QQ是这样的 蓝色、淡绿、淡红、深红代表了4种不同程度的Overdraw情况,1x、2x、3x和4x分别表示同一像素上同一帧的时间内被绘制了多次,1x就表示一次(最理想情况),4x表示4次(最差的情况),而我们做性能优化时,考虑消除的就是3x和4x。 其次是自定义view时的过度绘制,我们知道,自定义View的时候有时会重写onDraw方法,但是Android系统是无法检测onDraw里面具体会执行什么操作,从而系统无法为我们做一些优化。这样对编程人员要求就高了,如果View有大量重叠的地方就会造成CPU、GPU资源的浪费,此时我们可以使用canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视,还有clipPath()也是可以减少过度绘制的,只不过可能效果甚微。 合理布局在Android种系统对View进行测量、布局和绘制时,都是通过对View树的遍历来进行操作的。如果一个View树的高度太高就会严重影响测量、布局和绘制的速度。Google设计嵌套View最多是10层否则会崩溃。现在版本种Google使用RelativeLayout替代LineraLayout作为默认根布局,目的就是降低LineraLayout嵌套产生布局树的高度,从而提高UI渲染的效率。一下是合理布局的一些建议
那么怎样更直观地看自己App的布局层级呢?AS已经为我们集成了这么一个工具,具体打开的地方(需启动一个app): Tools -> Layout Inspector 左边是你app的布局树,中间是布局预览,右边是布局属性。借此可以全局分析你的app布局,就没必要再去每一个xml布局去看了。 WebVeiw优化WebView也是UI的一个部分,虽然html界面布局我们改变不了,但是我们可以通过WebView的用法去提高webview的性能。 webview提前初始化我们知道每个页面在打开时都会调用setContentView()方法 -> inflate() -> createViewFromTag(),也就是说都会调用view的构造函数,webview也不例外,但是不同的是webview的首次构造耗时比较长。我们可以测试一下 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.button).setOnClickListener { test() test() } } fun test() { val start = System.currentTimeMillis() WebView(App.appContext) val stop = System.currentTimeMillis() Log.d("RQ", "test: ${stop - start}") } } 复制代码 打印 2022-07-12 20:15:07.432 29656-29656/com.example.improvetest D/RQ: test: 167 2022-07-12 20:15:07.435 29656-29656/com.example.improvetest D/RQ: test: 3 复制代码 可以看到第二次初始化webview的时间远小于第一次,这是为什么捏?因为它要加载Webview内核,这是一个重量级的操作,内核是以apk的形式存在。而内核加载后在同一页面是共享的,因此后续的初始化时间就很少了。 那知道了这个我们可以提前初始化一个webview,减少后续webview初始化的时间。 WebView硬件加速致使页面渲染闪烁4.0以上的系统我们开启硬件加速后,WebView渲染页面更加快速,拖动也更加顺滑。但有个反作用就是,但有的时候可能会出现页面闪烁的情况,解决这个问题的方法是在闪烁前将WebView的硬件加速临时关闭,之后再开启,代码以下: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { //关闭硬件加速 //webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null) //开启硬件加速 //webview.setLayerType(View.LAYER_TYPE_HARDWARE, null) } 复制代码 增加进度条在网络不是很好的情况下,加载页面会出现白屏的情况,虽然我们不能改变,但是我们可以增加一个进度条来让用户知道加载进度,这也算是提升了性能了吧。具体代码如下: webView.webChromeClient = object :WebChromeClient(){ override fun onProgressChanged(view: WebView?, newProgress: Int) { if(newProgress==100){ pg1.setVisibility(View.GONE);//加载完网页进度条消失 } else{ pg1.setVisibility(View.VISIBLE);//开始加载网页时显示进度条 pg1.setProgress(newProgress);//设置进度值 } } } 复制代码 其他如果webveiw在你的应用中占比很高,很重要,还可以将webview做成一个独立进程(如果有能力),然后用aidl,messager,content provider,广播等来跨进程通信,这样webview就不会影响原app的性能。比如QQ,微信,微信的第一次重构就将webview做成了独立的进程。 webview我用的也不是很多,把一些我们可能用得上一些问题的做法给大家分享了一些,如果还觉得不够细致,具体可看看Android WebView 优化梳理 - 掘金 (juejin.cn) 卡顿优化卡顿优化其实前面也分析过了,UI绘制卡顿呐,启动慢导致的卡顿呐等等,具体见启动优化和渲染优化。这里说说卡顿到极致----ANR之后如何解决。 ANR问题分析ANR(Application Not responding)问题一般出现在Activtiy5秒之内无法响应屏幕触摸事件或者键盘输入事件,而BroadcastReceiver如果10秒之内还未执行完操作也会ANR。在实际开发中,ANR是很难从代码上发现的,那么我们应该怎么定位问题呢?其实,当一个进程发生ANR以后,系统会在/data/anr目录下创建记录ANR问题的文件,通过分析这些文件就能定位ANR的位置。 这里我们模拟一下ANR,主界面就一个按钮,然后给按钮注册监听: class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val button = findViewById<Button>(R.id.button) button.setOnClickListener { testANR() } } private fun testANR() { Thread.sleep(30*1000) } } 复制代码 之后点击按钮两次你就会看到ANR或者直接崩溃。之后我们就假装不知道ANR的位置,开始分析问题。 在老版本系统(Android8.1以下)的手机上,可以直接利用adb pull /data/anr/traces.txt 命令进行日志导出。 在新系统中用这个命令是无法导出的,它会提示你权限不够。那么怎么办呢,我们可以通过adb bugreport [导出目录]进行导出,这个会导出一大堆东西(我们只挑选有用的)。比如在控制台执行adb bugreport E:\test ,他会从手机中导出一个zip包到电脑的E:\test目录,会有导出进度显示: 导出完成: 随后找到导出的文件,解压缩,在/FS/data/anr目录下可以找到程序中的ANR日志。 打开日志文件大致浏览一下: 可以很明显看到是MainActivtiy里面的onCreate里面的按钮的点击事件的testANR方法里面的Thread.sleep造成的ANR,于是我们就可以痛快地解决问题啦。 当然,实际问题可能比这个更复杂,这里只是告诉大家这么一个方法,到时候就具体问题具体分析。 这里列出一些常见的ANR原因
网络优化App的网络连接对于用户来说, 影响很多, 且多数情况下都很直观, 直接影响用户对这个App的使用体验. 其中较为重要的两点:
如何监测app的网络情况监测app网络的工具有很多,比如AS自带的,Fiddler代理工具等等。代理工具就不说了,有很多。这里介绍AS自带的工具如何使用。 启动地方 AS -> App Inspection 然后就是这样的 中间的是网络监听状况,左边的是数据库监听状况,最右边的是后台服务的监听状况,看英文应该也好理解。数据库监听是这样的 什么表名啊,列都有,存的内容也有。网络监听是这样的 蓝色的是下载文件的速度,橙色的是上传文件的速度。后台服务的就不展示了,大家可以试试看。 优化方案合理使用网络缓存适当的使用缓存,不仅可以让我们的应用看起来更快,也能避免一些不必要的流量消耗,带来更好的用户体验,我们可以对设备的使用状态进行监听,在wifi下可以缓存一部分图片。比方说Splash闪屏广告图片,我们可以在连接到Wifi时下载缓存到本地;新闻类的App可以在Wifi状态下做离线缓存 限制访问次数我们在开发app过程中有的时候会设置一个按钮,然后点击按钮发送请求,这样其实不是最优做法,如果我点击很多次按钮,就会在短时间内发送多次请求,那么就会浪费流量,也很消耗app的性能。所以我们需要限制访问次数,两种方案
不同状态展现不同页面加载时显示好康的动画,留住用户,加载失败也要展现好康的动画给用户看(别直接崩溃了)。 其实说了这么多,一个好的网络请求框架就可以解决这些网络优化的问题,把这些解决方案封装在自己的网络请求框架里是最好的选择。 耗电优化现今,我们可能对流量都不是很缺,而且基本每家都有wifi,相较与流量我觉得一个app的耗电对用户更加敏感,现在市面上的手机基本上都有监控每个app的耗电功能,比如我的 可以看到QQ后台耗电多,抖音前台耗电多,但是这是QQ,没办法都得用,如果我们自己的app可能就被卸载了。那么我们先来分析一下为什么会耗电,盗用网上一张图就是 事实上就是软件调用硬件而产生了耗电,那有哪些硬件是可以控制的捏? 有这么这么多,我们就看几个常用的,CPU、GPU、Video、Audio、GPS、Network Video、Audio在使用这些功能的使用时候,他牵涉的不单单一个元器件的问题,而是更多,所以我们在使用这些功能的时候要做到离开即刻关闭释放。这两个组件用的最多的可能就是短视频和直播app了,如果出现这部分耗电严重,可以看看这些解决方案:
Network无线网络包括移动网络和wifi两种情况。移动网络是比wifi更加耗电的。 移动网络 移动网络数据传输有3种状态: 高功率状态:网络激活,允许设备以最大传输速率进行传输。 低功率状态:传输速率低于15kbps,耗电是高功率状态的一半,一般不能直接从程序中进入该状态,而是由高功率状态降级进入。 空闲状态:没有数据连接需要传输,耗电最少。可以看出,三种状态耗电不同,要使耗电最低应该尽量保持状态在空闲或低功率下。从空闲状态转换到高功率状态大概需要2s,从低功率状态转换到高功率状态需要1.5s。 应用中每创建一个网络连接,网络射频都会转到高功率状态,数据传输完毕降回低功率状态,降回过程需要5s,这5s耗电量保持在高功率状态,低功率降回到空闲状态需要12s,期间一直保持低功率状态。所以每次的数据传输都将导致将近20s电量的消耗。 WIFI网络 WIFI在active状态下有4种模式:低功率、高功率、低传输、高传输。 当从低(高)功率状态传输数据时,WIFI会暂时进入相应的低(高)传输状态,一旦数据传输完毕就回到初始状态。WIFI耗电是受包率(每秒接收和发送的数据包)和网速因素影响的。如果因素良好,即网络良好时,数据传输的很快,所以WIFI的高功率状态维持时间很短。这也就是为什么说移动网络耗电高于WIFI耗电,因为同样的数据大小传输时,移动网络固定状态转换就需要近20s的电量消耗。通过上面了解了网络连接过程,应该心里有了大概的优化建议。 网络耗电优化方案:
CPUcpu作为计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元。线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。通过上面的两个概念我们大概知道,一个我们负责设备运算和控制的元器件,一个是程序运算调度的最小单位。 CPU被高频次使用大概有以下几个原因:
其他我们用的多是GPS定位、Sensor遥感,只有当我们需要的时候才去打开这些硬件资源,并且及时释放,就能做到电量使用最优了。 接下来介绍一下AS对手机电量监控的工具,具体打开方式:AS -> Profiler -> Energy 其实跟之前看CPU和内存差不多,鼠标放上去能看到CPU、Network、Location的耗电程度,大致分为None、Light(轻)、Medium(中)、Heavy(严重) 当然,还有个更好的检测软件,叫Battery Historian,这里就不演示了,可自行上网查询。 总结通过对性能优化的学习,我发现他涉及的知识是方方面面的,像AMS、PMS、WMS、hook、启动流程等等等等,所以我觉得要真正做到性能优化,对这些一定要很了解的,不然完全不知道从哪下手。同时,这篇文章肯定还存在不足,可能也有错误,如果大家发现了都可以提出来。 |
2022-04-23
2022-01-26
2021-11-15
2021-08-02
2019-12-15