广告位联系
返回顶部
分享到

Android性能优化的详细介绍

Android 来源:互联网 作者: RQ527 发布时间:2022-09-17 18:52:24 人浏览
摘要

性能优化是一个app很重要的一部分,一个性能优良的app从被下载到启动到使用都能给用户到来很好的体验。自然我们做性能优化也是从被下载(安装包优化)、启动(启动优化)、使用

性能优化是一个app很重要的一部分,一个性能优良的app从被下载到启动到使用都能给用户到来很好的体验。自然我们做性能优化也是从被下载(安装包优化)、启动(启动优化)、使用(渲染优化、耗电优化、内存优化.........)等入手。因为我也是个菜鸟,所有东西都是现学的,所以过程中有任何问题都可以提出来,大家一起长知识。

安装包优化

当今手机的内存普遍是128G或者256G,当用户长时间使用,产生了大量数据后,留给app安装的空间可能只有几十个G,甚至更少。所以一个app的大小可能就决定了用户是否选择你。

优化方案:
  1. 清理无用资源

    在app打包的时候一些废弃的代码和无用的资源可能也会被打包,这无疑会增加app的体积。好在Android Studio有这么一个检测无用资源和代码的功能。具体方法是【Refactor】->【Remove Unused Resources..】

    再点击【Preview】可查看和选择无用的资源和代码。

  2. 使用Lint工具检查代码

    Android-Lint是as集成的一个代码检查工具,它可以检测图片是否重复,优化xml布局等等。

    具体使用是Android Studio -> 【Code】-> 【Inspect Code】

    Lint问题的种类:

    • Correctness 不够完美的编码,比如硬编码、使用过时 API 等
    • Performance 对性能有影响的编码,比如:静态引用,循环引用等
    • Internationalization 国际化,直接使用汉字,没有使用资源引用等
    • Security 不安全的编码,比如在 WebView 中允许使用 JavaScriptInterface 等
    • Usability 可用的,有更好的替换的 比如排版、图标格式建议.png格式 等
    • Accessibility 辅助选项,比如ImageView的contentDescription往往建议在属性中定义 等

    具体的一些问题种类的细分我这里就不多说了,可以看看这篇博客:

    Android性能优化之 Android Lint - 灰信网(软件开发博客聚合) (freesion.com)

  3. 使用shrinkResources

    我们知道缩小APK大小的方法除了开启混淆外

     minifyEnabled true
    复制代码

    还有

     shrinkResources true
    复制代码

    这里说一下,minifyEnabled 是用来删除无用的代码,shrinkResources是用来删除无用的文件(但其实不是真正的删除,只是保留文 件名但是没有内容)。还有要注意,shrinkResources需要与minifyEnabled 来配合使用,只有当minifyEnabled 为true的时候 shrinkResources才会起作用。但是有时候我们可能添加了一张图片只是作为验证,并未引用,这时候shrinkResources可能就会误删,怎么办呢?很简单,新增一个res/raw/keep.xml文件,并在文件如下编码

     <?xml version="1.0" encoding="utf-8"?>
     <resources xmlns:tools="http://schemas.android.com/tools"
         tools:keep="@drawable/xxxx,@layout/xxxxx"/>
    复制代码

    keep里面就列举需要保留误删的资源。

    注:string.xml中没有被引用的怎么设置都不会被删除,shrinkResources删除的只是drawable和layout

  4. 资源压缩

    在Android中,使用的图片是比较多的,这些图片是很占用资源的,对图片进行压缩和择优选择也是app瘦身的一种方案。

    (1)使用tinypng等图片压缩工具对图片进行压缩,然后替换之前的图片

    (2)尽量将图片都用Webp格式的,其次是JPG格式,再是PNG格式

    (3)使用SVG,矢量图能比位图节约30%~40%的空间

    (4)尽量不要在项目中使用帧动画,一秒就十几张图片也是很耗内存的,使用Lottie等方案

    (5)重用Bitmap,不使用了记得回收

    (6)可以使用微信开源资源文件混淆工具——AndResGuard。一般可以压缩apk的1M左右大。

  5. 其他方法

    • 动态加载so库文件,插件化开发;
    • 统一第三方库,在满足需求的前提下选择体积更小的库,仅引入需要的代码。比如图片加载库,按缓存的需要来我们可以对图片加载库做个排序:Picasso < Android-Universal-Image-Loader < Glide < Fresco,Fresco体积比较大,一般用于图片缓存量比较大的app,比如壁纸app,一般Glide可满足日常需求,Picasso体积最小,它与和Square的网络库一起能发挥最大作用,因为Picasso可以选择将网络请求的缓存部分交给了okhttp实现;
    • 避免使用枚举,可能几十个枚举才相当于一张图片,但是积少成多嘛;
    • 在多国语言需求不大的情况下可以删除其他国家的语言,只保留中文和英文。
    • 再深入一点的还有字节码优化等等等

启动优化

启动优化可以说是性能优化里很重要很重要的一个部分了,用户拿到你的app,第一印象自然是app启动的界面,app启动的流畅度和时间长短,可以说启动性能就是一个app的门面。(最讨厌app启动时候的广告了)

大家可能都听说过2-5-8原则:

  • 当用户在0-2秒之间得到响应时,会觉得系统响应得很快
  • 当用户在2-5秒之间得到响应时,会感觉系统的响应速度还可以
  • 当用户在5-8秒之间得到响应时,会感觉系统响应得速度很慢,但是还能接受
  • 当用户在超过8秒还无法得到响应时,会感觉系统很垃圾,认为系统已经挂了

所以不管你的app做的再怎么牛逼,用户点进你的app,反应速度让他很失望,用户也无继续使用的欲望。那么我们应该如何去规划整体的启动优化呢?具体方案如下:

冷启动、热启动和温启动

什么是冷启动、热启动、温启动?

  • 冷启动:系统不存在App进程(如APP首次启动或APP被完全杀死)时启动App称为冷启动。
  • 热启动:按了Home键或其它情况app被切换到后台,再次启动App的过程。
  • 温启动:温启动包含了冷启动的一些操作,不过App进程依然存在,这代表着它比热启动有更多的开销。

由此可见启动最慢的是冷启动,最快的是热启动。着重优化的地方也是冷启动。

在冷启动下会进行如下的相关流程

与我们代码相关的只有创建Application之后到首帧绘制之前。

  1. Application创建

    当Application启动时,空白的启动窗口将保留在屏幕上,直到系统首次完成绘制应用程序。此时,系统进程会交换应用程序的启动窗口,允许用户开始与应用程序进行交互。这就是为什么我们的程序启动时会先出现一段时间的黑屏(白屏),但其实市面上很多app启动都是有一个logo的,再是页面。

    如何解决?

    在themes.xml定义一个主题

     <style name="WelcomeTheme" parent="Theme.AppCompat.NoActionBar">
             <!--设置背景颜色或者图片-->
             <item name="android:windowBackground">@drawable/xxxx</item>
             <!--设置没有ActionBar-->
             <item name="android:windowNoTitle">true</item>
             <!--设置顶部状态栏颜色-->
             <item name="android:statusBarColor" >@color/xxxx</item>
      </style>
    复制代码

    img

    但是这样也改变了activity启动后的theme,所以还得在onCreate方法中将主题还原,即

     override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
             setTheme(R.style.Theme_Universe); //恢复原有的样式
             setContentView(R.layout.activity_main)
         }
    复制代码

    当然也可另外用一个activity来用作启动的activity,在里面也可以做一些延时的操作或者加入开屏广告什么的一些操作。

    我们很多时候并不是用系统默认的Application,更多的时候是自定义一个MyApplication,然后在里面做一些初始化的操作。但是如果需要初始化的东西太多了,比如友盟,Bugly,网络请求库,图片加载库,ARouter等,势必会拖慢app的启动速度。那怎么办呢?这些又都是必须要的。只能异步加载了,或者等应用内启动之后再初始化。这里给出一些比价简单的优化操作:

    1. 可以在Application中封装两个方法一个onSyncLoad,一个onAsyncLoad。比如像友盟,Bugly这样的业务非必要的可以的异步加载。可以放在onAsyncLoad中初始化;对于图片,网络请求框架就放在onSyncLoad中初始化。可能有人会觉得onAsyncLoad中异步会额外开销一个Thread,但其实当一个app体量变大后,开销一个Thread带来的收益是远远大于原来同步初始化的
    2. 我们知道ContentProvider作为Android四大组件之一,它的onCreate方法是在Application.attachBaseContext() 和 Application.onCreate()之间执行的(原理后面说),所以我们也可以间接使用它来初始化操作以减轻Application的负担,这也是很多第三方库的做法,比如LeakCanary、Picasso。但是这样也有弊端,要知道ContentProvider属于四大组件之一也是比较重量级的,据测试,一个空ContentProvider启动就耗时2ms,如果数量再增加,那么可能性能优化就得不偿失了。所以JetPack新成员App Startup就诞生了。具体App Startup的使用非常简单,这里就不多说了,给出郭霖的文章:mp.weixin.qq.com/s/rverE0OGR…
    3. 再比如地图,推送等,非第一时间需要的可以在主线程做延时启动。当程序已经启动起来之后,在进行初始化。
  2. Activity创建

Activity里面的优化和Application差不多,但是Activity.onCreate方法的开销是最大的,对整个app启动的影响也最大,所以绝对不能再里面执行太耗时的操作。其次是对布局优化也可以缩短onCreate的时间,具体见渲染优化。

这里再介绍几个用于检测app启动性能的工具:

  • 最简单的就是as自己的日志工具

    D

    搜索词是Displayed,右边选No Files,然后就能看到各个Activity的启动时间了。

  • 在Terminal中输入adb shell am start -W 包名/包名.首屏Activity 这一行命令就能看到页面的启动时间

    T

    控制台输出了以下信息

     Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.universe/.view.MainActivity }
     Status: ok
     LaunchState: COLD
     Activity: com.example.universe/.view.MainActivity
     TotalTime: 3447
     WaitTime: 3450
     Complete
    复制代码

    简单说一下

    • LaunchState:代表启动方式
    • TotalTime:代表启动时间,包含创建进程+Application 初始化+Activity 初始化到界面显示。
    • WaitTime: 一般比TotalTime 大点,包含系统影响的耗时
  • 借助优化检测工具

    TraceView

    TraceView是以图形的形式展示执行时间、调用栈等信息,信息比较全面,包含所有线程。 使用TraceView检测生成生成的结果会放在Andrid/data/packagename/files路径下。因为Traceview收集的信息比较全面,所以会导致运行开销严重,整体APP的运行会变慢,因此我们无法区分是不是Traceview影响了我们的启动时间。AS已经为我们内置了TraceVeiw,直接用

    具体使用是AS - > 【ProFiler】-> 右侧SESSIONS 旁边的+号 -> 选择你的手机,在选择一个进程 就会出现这样的画面

    F

    图中A是一些事件的响应,比如点击,屏幕翻转等等

    B是CPU的一些使用情况,这里会说

    C是内存的一些使用情况,等会讲内存优化的时候会说

    D是电量的一些情况,讲耗电优化会说

    E是时间轴

    这里我们点击B区,就会出现下列界面

    G

    同样,A是一些事件的响应,B是CPU的执行情况,C是线程列表和线程占用CPU的情况,D是时间轴,E是记录这些情况成文件进行更加细致的分析,这里就不说明了,可以百度其用法。

    以此我们可以更加直观的看出CPU的使用情况,从而找出问题解决问题。

    SysTrace

    Systrace是结合Android内核数据,生成HTML报告,从报告中我们可以看到各个线程的执行时间以及方法耗时和CPU执行时间等。它比TraceView更轻量,但用法差不多,具体用法可参考:Android Systrace使用介绍 - 简书 (jianshu.com)

内存优化

在Android的虚拟机中,每fork一个进程,它的内存是给定的,因为移动设备的内存相对PC比较小,资源紧张,因此一个app在运行过程中一定要管理好自己的那部分内存,以提高稳定性。在内存使用中经常出现的问题也是内存抖动和内存泄漏了。

内存抖动

内存抖动是由于短时间内有大量对象进出JVM的新生区导致的,内存忽高忽低,有短时间内上升和下落的趋势,分析图成锯齿状。

它伴随着频繁的GC(Garbage Collection垃圾回收),频繁GC会大量占用UI线程和CPU资源,会导致APP整体卡顿,甚至OOM。

先说为什么频繁GC会导致APP整体卡顿?

在JVM的GC机制中,垃圾回收有单线程收集和多线程收集,但不管是哪种回收方式,在回收的时候所有用户线程都会被暂停(STW),具体原理涉及JVM的知识了,就不再深入了。所以频繁地GC,用户线程就会被频繁地暂停,自然app就会卡顿。

为什么频繁GC也有可能会OOM?

先看一张图

A

这里简单说一下JVM的空间担保机制,简单理解就是Java堆划分为新生代 (Young Generation)和老年代(Old Generation)两个区域,新生代空间比较少,只有1/3,而老年代有2/3,新生代中不断有对象被创建然后回收,只有少部分仍然存在的对象会进入老年代。而当频繁GC时,会导致新生代中有大量对象被创建,然后新生代空间就会不够用,这时候老年代就会划分一部分空间用来给新生代创建大量的对象。这就是JVM的空间担保机制。但是当老年代被划出一部分空间后,假如这时候有一个比较大的对象,比如一张图片,从新生区转移到了老年区,但是这时候老年区被缩小了,剩下的空间不够了,这时候就触发了OOM。

怎么监测内存抖动?

AS有自带的检测内存抖动的工具-----Memory Monitor

其实这个在启动优化工具里面也提到过。

打开方式:Profiler ->SESSIONS右边的加号选择你的手机在选择你的app 就会出现这样的界面

F

这次我们不点B,选择C区Memory,这时候就会出现如下界面

B

A依然是一些事件的反应,B是内存使用的图形化显示,C是鼠标放在图形上就会有各个语言占用内存情况,D是时间轴,但这是内存使用正常的情况,当出现频繁GC的情况时

是这样滴,底部还会有一排垃圾桶表示频繁回收。那么如何定位呢?我们看到左侧有三个选项:

  • Capture heap dump:捕获堆转储,什么是堆转储?就是java的内存快照,简单来说就是把这些内存记录写入一个文件,文件类型是hprof,然后进行更细致的分析。更多的时候是结合MAT(Memory Analyzer tool)来分析内存泄漏,这也是比较老的方法,大家可查阅了解一下,但是这种方法比较低效(搞不好as会卡死),现在检测内存泄漏有更方便的工具---LeakCanary
  • Record native allocations:记录native相关对象的内存分配
  • Record java/kotlin allocations:记录java/kotlin相关对象的内存分配

这里一般发生内存抖动都是由于频繁创建java/kotlin对象引起的,所以我们选择第三个并点击Record,等待一会就会出现这样的界面

A

上面一排排的垃圾桶就表示在频繁GC,下面的表格显示了各个对象内存分配情况,我们点击最多的char数组

A

跟踪可以发现是stringPlus相关操作引起的GC频繁,再看String

A

这里就追踪到了,原来是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回收呢?看一张图

A

在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为我们生成的发生内存泄漏对象的引用树

A

可以很明显看到是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源码分析可以看看我的另外一篇文章,下面是一些常见的内存泄漏:

  1. 单例模式引发的内存泄漏

    原因:单例模式里的静态实例持有对象的引用,导致对象无法被回收,常见为持有Activity的引用

    优化:改为持有Application的引用,或者不持有使用的时候传递。

  2. 集合操作不当引发的内存泄漏

    原因:集合只增不减

    优化:有对应的删除或卸载操作

  3. 线程的操作不当引发的内存泄漏

    原因:线程持有对象的引用在后台执行,与对象的生命周期不一致

    优化:静态实例+弱引用(WeakReference)方式,使其生命周期一致

  4. 匿名内部类/非静态内部类操作不当引发的内存泄漏

    原因:内部类持有对象引用,导致无法释放,比如各种回调

    优化:保持生命周期一致,改为静态实例+对象的弱引用方式(WeakReference)

  5. 常用的资源未关闭回收引发的内存泄漏

    原因:BroadcastReceiver,File,Cursor,IO流,Bitmap等资源使用未关闭

    优化:使用后有对应的关闭和卸载机制

  6. Handler使用不当造成的内存泄漏

    原因:Handler持有Activity的引用,其发送的Message中持有Handler的引用,当队列处理Message的时间过长会导致Handler无法被回收

    优化:静态实例+弱引用(WeakReference)方式

渲染优化

在上一章我们说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渲染的效率。一下是合理布局的一些建议

  1. 布局重用,对于多次重用的布局使用标签来达到重用的目的,对于根布局一样的,可使用标签取消冗余的viewgroup。比如我我们使用标签的时候可能include里面的布局最外层是,而在外部的外层布局也是,这时候就可以用标签替换里面的,然后系统就会把include的布局放到外部的LinearLayout而忽视merge,从而减少一层嵌套。
  2. 对于一些复杂的布局我们有时候是不需要一来就全部加载的,这时候就可以用标签来实现延迟加载,那有人可能会问,我直接设置控件的visible和invisible不行吗,是可以。但是设置visibility属性布局依然会被加载,只是不显示罢了,而VeiwStub只有被设置成visible时才会被加载。
  3. 减少布局层级当布局层级太多的时候可以考虑Constranlayout,这个布局性能很好,适配好还能减少布局间的嵌套,其次可以考虑RelaticeLayout。
  4. 减少不必要的背景设置,减少复杂shape等。能用父布局的背景,子布局就没必要再设置背景。

那么怎样更直观地看自己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目录,会有导出进度显示:

D

导出完成:

D

随后找到导出的文件,解压缩,在/FS/data/anr目录下可以找到程序中的ANR日志。

A

打开日志文件大致浏览一下:

可以很明显看到是MainActivtiy里面的onCreate里面的按钮的点击事件的testANR方法里面的Thread.sleep造成的ANR,于是我们就可以痛快地解决问题啦。

当然,实际问题可能比这个更复杂,这里只是告诉大家这么一个方法,到时候就具体问题具体分析。 这里列出一些常见的ANR原因

  • 主线程阻塞或主线程数据读取

解决办法:避免死锁的出现,使用子线程来处理耗时操作或阻塞任务。尽量避免在主线程query provider、不要滥用SharePreferenceS

  • CPU满负荷,I/O阻塞

解决办法:文件读写或数据库操作放在子线程异步操作。

  • 内存不足

解决办法:AndroidManifest.xml文件中可以设置 android:largeHeap="true",以此增大App使用内存。不过不建议使用此法,从根本上防止内存泄漏,优化内存使用才是正道。

  • 各大组件ANR

各大组件生命周期中也应避免耗时操作,注意BroadcastReciever的onRecieve()、后台Service和ContentProvider也不要执行太长时间的任务。

网络优化

App的网络连接对于用户来说, 影响很多, 且多数情况下都很直观, 直接影响用户对这个App的使用体验. 其中较为重要的两点:

  • 流量 :App的流量消耗对用户来说是比较敏感的,毕竟流量是花钱的嘛.。现在大部分人的手机上都有安装流量监控的工具App,用来监控App的流量使用。如果我们的App这方面没有控制好,会给用户不好的使用体验。
  • 用户等待 :也就是用户体验,良好的用户体验,才是我们留住用户的第一步。如果App请求等待时间长,会给用户网络卡,应用反应慢的感觉,如果有对比,有替代品,我们的App很可能就会被用户无情抛弃。

如何监测app的网络情况

监测app网络的工具有很多,比如AS自带的,Fiddler代理工具等等。代理工具就不说了,有很多。这里介绍AS自带的工具如何使用。

启动地方 AS -> App Inspection

A

然后就是这样的

中间的是网络监听状况,左边的是数据库监听状况,最右边的是后台服务的监听状况,看英文应该也好理解。数据库监听是这样的

A

什么表名啊,列都有,存的内容也有。网络监听是这样的

A

蓝色的是下载文件的速度,橙色的是上传文件的速度。后台服务的就不展示了,大家可以试试看。

优化方案

合理使用网络缓存

适当的使用缓存,不仅可以让我们的应用看起来更快,也能避免一些不必要的流量消耗,带来更好的用户体验,我们可以对设备的使用状态进行监听,在wifi下可以缓存一部分图片。比方说Splash闪屏广告图片,我们可以在连接到Wifi时下载缓存到本地;新闻类的App可以在Wifi状态下做离线缓存

限制访问次数

我们在开发app过程中有的时候会设置一个按钮,然后点击按钮发送请求,这样其实不是最优做法,如果我点击很多次按钮,就会在短时间内发送多次请求,那么就会浪费流量,也很消耗app的性能。所以我们需要限制访问次数,两种方案

  1. 限制按钮的点击次数
  2. 封装网络请求框架,在框架里限制同一时间访问的次数
不同状态展现不同页面

加载时显示好康的动画,留住用户,加载失败也要展现好康的动画给用户看(别直接崩溃了)。

其实说了这么多,一个好的网络请求框架就可以解决这些网络优化的问题,把这些解决方案封装在自己的网络请求框架里是最好的选择。

耗电优化

现今,我们可能对流量都不是很缺,而且基本每家都有wifi,相较与流量我觉得一个app的耗电对用户更加敏感,现在市面上的手机基本上都有监控每个app的耗电功能,比如我的

可以看到QQ后台耗电多,抖音前台耗电多,但是这是QQ,没办法都得用,如果我们自己的app可能就被卸载了。那么我们先来分析一下为什么会耗电,盗用网上一张图就是

img

事实上就是软件调用硬件而产生了耗电,那有哪些硬件是可以控制的捏?

img

有这么这么多,我们就看几个常用的,CPU、GPU、Video、Audio、GPS、Network

Video、Audio

在使用这些功能的使用时候,他牵涉的不单单一个元器件的问题,而是更多,所以我们在使用这些功能的时候要做到离开即刻关闭释放。这两个组件用的最多的可能就是短视频和直播app了,如果出现这部分耗电严重,可以看看这些解决方案:

  1. 线程数是否暴增。
  2. 弹幕是否做到复用了,是否存在内存泄露问题。
  3. 动画特效是否及时释放,执行效率是否很快。
  4. 承载功能的实例是否存在多份。
  5. 检查内存、cpu使用情况。
Network

无线网络包括移动网络和wifi两种情况。移动网络是比wifi更加耗电的。

移动网络

移动网络数据传输有3种状态:

高功率状态:网络激活,允许设备以最大传输速率进行传输。

低功率状态:传输速率低于15kbps,耗电是高功率状态的一半,一般不能直接从程序中进入该状态,而是由高功率状态降级进入。

空闲状态:没有数据连接需要传输,耗电最少。可以看出,三种状态耗电不同,要使耗电最低应该尽量保持状态在空闲或低功率下。从空闲状态转换到高功率状态大概需要2s,从低功率状态转换到高功率状态需要1.5s。

应用中每创建一个网络连接,网络射频都会转到高功率状态,数据传输完毕降回低功率状态,降回过程需要5s,这5s耗电量保持在高功率状态,低功率降回到空闲状态需要12s,期间一直保持低功率状态。所以每次的数据传输都将导致将近20s电量的消耗。

WIFI网络

WIFI在active状态下有4种模式:低功率、高功率、低传输、高传输。

当从低(高)功率状态传输数据时,WIFI会暂时进入相应的低(高)传输状态,一旦数据传输完毕就回到初始状态。WIFI耗电是受包率(每秒接收和发送的数据包)和网速因素影响的。如果因素良好,即网络良好时,数据传输的很快,所以WIFI的高功率状态维持时间很短。这也就是为什么说移动网络耗电高于WIFI耗电,因为同样的数据大小传输时,移动网络固定状态转换就需要近20s的电量消耗。通过上面了解了网络连接过程,应该心里有了大概的优化建议。

网络耗电优化方案:

  1. 文本和文件压缩传输。 不管发送还是请求数据,在数据传输过程中使用gzip(Gzip是传输时将文件压缩传输的一种技术,okhttp默认是使用了gzip的)将数据进行压缩。经过压缩的数据需要更短的时间传输即可完成,这样使得无线所处的高功率状态时间更短,从而减少了耗电。
  2. 精简文本文件,去掉文本中空行、空格、注释等无意义内容。
  3. 请求一个图片时,客户端提供一个分辨率大小,服务器根据分辨率把裁剪缩放后的图片给客户端返回,采用使用webp图片。(节省传输时间)
CPU

cpu作为计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元。线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。通过上面的两个概念我们大概知道,一个我们负责设备运算和控制的元器件,一个是程序运算调度的最小单位。

CPU被高频次使用大概有以下几个原因:

  1. 程序运算复杂(高运算量),例如高精度等,导致CPU满负荷运载,这里优化可能就设计数据结构、算法啥的。

  2. 程序线程短时间内无规则抢占CPU资源。

  3. wakelock唤醒。wakelock是什么?

    为了延长电池的使用寿命,Android设备会在一段时间后使屏幕变暗,然后关闭屏幕显示,最后停止CPU。WakeLock是一个电源管理系统服务功能,应用程序可以使用它来控制设备的电源状态。

    WakeLock可以用来保持CPU运行,避免屏幕变暗和关闭,以及避免键盘背光灯熄灭。

  4. 定时器(AlarmManager)。

其他

我们用的多是GPS定位、Sensor遥感,只有当我们需要的时候才去打开这些硬件资源,并且及时释放,就能做到电量使用最优了。

接下来介绍一下AS对手机电量监控的工具,具体打开方式:AS -> Profiler -> Energy

其实跟之前看CPU和内存差不多,鼠标放上去能看到CPU、Network、Location的耗电程度,大致分为None、Light(轻)、Medium(中)、Heavy(严重)

当然,还有个更好的检测软件,叫Battery Historian,这里就不演示了,可自行上网查询。

总结

通过对性能优化的学习,我发现他涉及的知识是方方面面的,像AMS、PMS、WMS、hook、启动流程等等等等,所以我觉得要真正做到性能优化,对这些一定要很了解的,不然完全不知道从哪下手。同时,这篇文章肯定还存在不足,可能也有错误,如果大家发现了都可以提出来。


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 : https://juejin.cn/post/7143999218963726372
相关文章
  • Kotlin的Collection与Sequence操作异同点介绍

    Kotlin的Collection与Sequence操作异同点介绍
    在Android开发中,集合是我们必备的容器,Kotlin的标准库中提供了很多处理集合的方法,而且还提供了两种基于容器的工作方式:Collection 和
  • 实现一个Kotlin函数类型方法

    实现一个Kotlin函数类型方法
    接口与函数类型 业务开发中,经常会有实现一个函数式接口(即接口只有一个方法需要实现)的场景,大家应该都会不假思索的写出如下代
  • Android10 App启动Activity源码分析
    ActivityThread的main方法 让我们把目光聚焦到ActivityThread的main方法上。 ActivityThread的源码路径为/frameworks/base/core/java/android/app/ActivityThread。 1 2
  • Android10客户端事务管理ClientLifecycleManager源码解析

    Android10客户端事务管理ClientLifecycleManager源码解析
    在Android 10 App启动分析之Activity启动篇(二)一文中,简单地介绍了Activity的生命周期管理器是如何调度Activity进入onCreate生命周期的流程。这
  • Kotlin对象的懒加载方式by lazy与lateinit异同介绍

    Kotlin对象的懒加载方式by lazy与lateinit异同介绍
    属性或对象的延时加载是我们相当常用的,一般我们都是使用 lateinit 和 by lazy 来实现。 他们两者都是延时初始化,那么在使用时那么他们两
  • Android类加载流程分析

    Android类加载流程分析
    本文分析的代码基于Android8.1.0源码。 流程分析 从loadClass开始,我们来看下Android中类加载的流程 /libcore/ojluni/src/main/java/java/lang/ClassLoader.ja
  • Android实现读写USB串口数据的代码

    Android实现读写USB串口数据的代码
    最近在研究USB方面的内容;先后做了关于Android读写HID、串口设备的DEMO。本文比较简单,主要介绍的是Android实现读取串口数据的功能 废话不
  • Epoxy - 在RecyclerView中构建复杂界面
    Diffing 对于复杂数据结构支持的多个视图类型展示在屏幕上, Epoxy此时是尤其有用的. 在这些场景中, 数据可能会被网络请求, 异步 Observable, 用
  • Android性能优化的详细介绍

    Android性能优化的详细介绍
    性能优化是一个app很重要的一部分,一个性能优良的app从被下载到启动到使用都能给用户到来很好的体验。自然我们做性能优化也是从被下
  • Android进阶宝典-插件化2(Hook启动插件中四大组件

    Android进阶宝典-插件化2(Hook启动插件中四大组件
    在上一节,我们主要介绍了如果通过反射来加载插件中的类,调用类中的方法;既然插件是一个apk,其实最重要的是启动插件中的Activity、
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计