安卓性能与稳定性相关问题

最近老是被人问,什么是anr,怎么排查oom?卡顿就卡顿,闪退就闪退,耗时就耗时,内存泄漏就内存泄漏,平时土到掉渣,一听到很官方的名字就反应不过来是什么。好,来捋一下。看看他们之间有什么关系。俺之前已经遇到过很多这些问题了记录一下,下次再遇到就知道怎么解决了。

目录

影响体验的性能问题

1.内存问题表现形式

2.内存抖动

3.内存泄露

1)内存泄漏的情况:

2)表现

3)怎么避免?

4)内存泄漏原理

5)内存泄漏检测原理

6)怎么检测?

7)LC原理和步骤

8)LC不能用于线上的原因

9)koom

4.OOM

1)场景场景:

2)排查方式:

3)解决方法:

5.耗时操作有哪些?

1)耗时操作:

2)解决方法:

3)之前遇到过:

4)首帧耗时

6.Fps

7.卡顿问题排查及原理

1)表现

2)原因

3)原理

4)绘制渲染

5)排查方式

8.anr是啥?原理是啥?

1)排查手段

9.包大小

1)影响

2)怎么优化?

10.保活

11.线上监控体系

兼容性问题、多语言适配、RTL


影响体验的性能问题

内存泄露/内存抖动/OOM、启动首帧耗时、卡顿、anr、包体积、fps、兼容性问题、保活

关联之前写过jvm&内存分析文章,做为内存部分的基础,里面讲了jvm内存模型、gc算法、profile、MAT工具具体使用方法。

JVM&内存分析_jvm内存分析-CSDN博客文章浏览阅读1.3k次,点赞27次,收藏12次。在硬盘里面将java代码编译成机器能够识别的字节码,JVM将这个字节码搂到内存里面,在内存里面分配一块空间,用堆来存储该对象,在栈里面进行计算。_jvm内存分析 https://blog.csdn.net/mix39/article/details/145408581?spm=1001.2014.3001.5502

1.内存问题表现形式

  • 内存抖动:锯齿状、GC频繁导致卡顿

  • 内存泄露:可用内存逐渐减少、频繁GC

  • 内存溢出:OOM、程序异常

2.内存抖动

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

  • 它伴随着频繁的GC,GC会大量占用UI线程和CPU资源,会导致APP整体卡顿,甚至OOM的可能。

所以说在耗时管控重的地方不能开个for循环频繁生成对象,不然ui一卡一卡的

3.内存泄露

1)内存泄漏的情况:
  1. 单例模式。

  2. 静态变量持有的引用导致的内存泄露。

  3. 非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象生命周期长时,就会内存泄露,比如常见的Handler,Thread,AsyncTask,创建一个线程去作网络请求。

  4. 未取消注册或回调导致的内存泄漏。

  5. 集合中的对象未清理造成内存泄漏。

  6. 资源未关闭或未释放导致内存泄漏,比如流对象、流式RPC、WebView、地图等使用完后要及时关闭。

  7. Handler造成的内存泄漏

  8. 线程造成的内存泄漏

  9. 地图、视频播放器、图片加载、webview、流式数据停止使用或者退出页面的时候没有反注册。

  10. 也不一定是页面,如果一个列表每个item都去注册一次webview,但是item不可见或者销毁的时候没有反注册,也可能会导致后面的数据渲染不出来。

2)表现

程序异常(case by case)

程序越使用内存占用越大且内存占用增长较快

频繁GC但是内存得不到有效的释放

因为频繁的GC和生成对象和内存碎片化界面响应越来越慢

界面渲染卡顿、滑动卡顿或者view组件渲染不出来又不报错、白屏

最终OOM闪退

3)怎么避免?

记得反注册

合理使用缓冲策略

handler使用匿名内部类,关联view或者acitivty使用weakrefrence,页面退出的时候remove所有消息

及时释放资源,如流式数据、webview

避免静态集合无限制增长

4)内存泄漏原理

创建一个对象,也就是在堆区里申请多一块空间给它,如果某个对象一直持有这块空间,该空间无法得到释放。所以如果内存泄露的次数多,最终很容易引起内存溢出。

程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。

对象在引用链上,但是已经不可用了。根可达,但是内存已经不能再用了。

5)内存泄漏检测原理

jvm判断对象应该被回收的方式:

引用计数法

当对象没有其他对象在引用该对象时,应该被回收。

可达性分析

GC roots(静态变量、线程栈变量、常量池、JNI指针)

在GC roots的引用链上可以找到对象的时候,就认为这个对象根可达,GC来回收时发现该对象根可达,该对象就不应该被回收。在引用链找不到该对象,该对象不可达,就应该被回收。

6)怎么检测?

自己可以用MAT,俩Ativity跳来跳去,比较heap堆栈,过滤掉弱引用虚引用啥的就是泄漏的方法

平时线下检测LeakCanary,主要是hook页面生命周期,hook生命周期这种操作也可以用来检测白屏。

7)LC原理和步骤

原理:可达性分析。根可达但无用。利用 WeakReference + ReferenceQueue 来检测对象是否应被回收。

8)LC不能用于线上的原因

LeakCanary 会主动GC、Dump内存快照,造成卡顿和ANR,只适合开发阶段,不能用于线上。

频繁gc造成掉帧卡顿

dump内存快照耗时会造成anr

hprof上传文件太大,耗时耗流

重复dump,手机内存爆满

9)koom

检测思路跟LC差不多,线上可以使用这个监控内存泄漏。

不同的是触发方式、gc时机不同、分析方式,还要可以检测native泄漏和thread泄漏。

koom不主动 GC,不阻塞主进程。内存占用率 >= 80% && 连续检测超过3次时,fork一个子进程去异步分析dump文件,卡也只卡子进程,且本地分析以后只把分析结果上传到服务器。

4.OOM

OOM(out of memory)。本质是系统给这块app分配的内存额度用完了,申请新内存是gc回收后依然凑不够需要的连续空间。

安卓给每个app设置了内存上限,128mb、512mb-1gb。到达这个上限就是用完了。一个是真的用完了,一个是可能腾不出来内存了,内存被占了无法释放,比如内存泄漏多了无法垃圾回收,这块内存无法再使用,如果新对象再进来就爆了

1)场景场景:

内存泄漏:

用完的对象忘了释放(比如Activity被静态变量持有),导致内存只增不减,最终撑爆。这是线上OOM的头号元凶。

大图/文件加载:

在低端机上直接加载一张几MB的原始高清大图,瞬间占满可用内存。

内存抖动:

在循环或高频方法(如onDraw)中大量new临时对象,导致频繁GC,当生成速度超过回收速度时爆仓。

2)排查方式:

内存泄漏见上面的内存泄漏排查方式。

其他的可以看日志out of memory能直接看到报错,可以查看java堆内存和native堆的占用情况,看哪个大对象占着不动。

内存抖动可以通过profile看到有没有频繁生成对象、gc。

3)解决方法:

内存泄漏各有各的解决办法

内存抖动就是让它减少抖动,比如减少对象生成,复用线程,线程池,message、减少gc

大图加载核心:先问尺寸,再算比例,最后只加载缩略图。 这样一张2000x2000的图,采样率设为16后,内存直接从 ~16MB(2000×2000×4字节)降到 ~0.06MB(125×125×4字节)。

需要预处理,裁剪和压缩,安卓艺术开发探索书上有。一般是采样压缩,即通过降低图片的分辨率来减少内存占用。
核心步骤
1.获取原始图片尺寸(不占内存)
首次调用 BitmapFactory.decodeXxx 时设置 inJustDecodeBounds = true,此时只解析图片的宽高(outWidth、outHeight),并不真正加载像素数据到内存。

2.根据图片容器宽高和图片素材宽高计算采样率 inSampleSize,让容器装得下图片
通过 calculateInSampleSize 计算一个 2 的幂次 的数值(如 1、2、4、8…),使得采样后的图片宽高恰好大于或等于目标宽高。
例如原始图 2000×2000,目标 100×100,采样率最终为 16,采样后图片变为 125×125。

3.按采样率解码
再次解码时将 inJustDecodeBounds = false,并设置 inSampleSize = 16。系统只会加载原图每 16×16 个像素中的一个像素,最终生成的 Bitmap 内存占用仅为原始内存的 1/256,从而避免 OOM。

特点
只降低分辨率,不改变图片格式,适合将大图显示到尺寸固定的 ImageView。采样率必须是 2 的幂(官方推荐),算法采用循环翻倍直到满足条件,保证解码效率。

/**
 * 图片压缩功能
 */
public class ImageResizer {
    private static final String TAG = "ImageResizer";

    public ImageResizer() {
    }

    /**
     * 高效加载图片:将裁剪后的图片传给ImageView,降低内存占用,从而避免OOM,提高Bitmap加载时的性能
     */
    public static Bitmap decodeSampledBitmapResource(Resources resources, int resourceId, int resourceWidth, int resourceHeight) {
        //1、将inJustDecodeBounds设为true并加载图片
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(resources, resourceId, options);
        //3、根据采样规则结合目标view所需大小计算出采样率
        options.inSampleSize = calculateInSampleSize(options, resourceWidth, resourceHeight);
        //4、将inJustDecodeBounds参数设为false,然后重新加载图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(resources, resourceId, options);
    }

    /**
     * 根据采样规则结合目标view所需大小计算出采样率
     */
    private static int calculateInSampleSize(BitmapFactory.Options options, int resourceWidth, int resourceHeight) {
        //2、取出图片的原始宽高信息(outWidth、outHeight)
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > resourceHeight || width > resourceWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
            while ((halfHeight / inSampleSize >= resourceHeight)
                    && (halfWidth / inSampleSize >= resourceWidth)) {
                //inSampleSize按照2的指数倍递增,直到图片的宽高小于ImageView
                inSampleSize *= 2;
            }
        }
        Log.d(TAG, "calculateInSampleSize: inSampleSize --> " + inSampleSize);
        return inSampleSize;
    }

    public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fileDescriptor, int resourceWidth, int resourceHeight) {
        //1、将inJustDecodeBounds设为true并加载图片
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
        //3、根据采样规则结合目标view所需大小计算出采样率
        options.inSampleSize = calculateInSampleSize(options, resourceWidth, resourceHeight);
        //4、将inJustDecodeBounds参数设为false,然后重新加载图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
    }
}

decodeSampledBitmapFromFileDescriptor与decodeSampledBitmapResource的区别

对比项decodeSampledBitmapFromFileDescriptordecodeSampledBitmapResource
数据源文件资源(外部存储)应用资源(res/)内部资源
密度处理不处理,直接原始尺寸自动根据屏幕密度缩放
采样基准图片真实物理尺寸经过密度缩放后的尺寸
典型用途加载用户照片、大图加载内置大图(如启动图)

一般用来加载本地大图可以使用这个。

5.耗时操作有哪些?

影响:

会造成ui卡顿、启动耗时、首帧耗时掉帧

1)耗时操作:

1)rpc、网络请求、数据库操作、文件读写、IO、file、进程间通信、编解码

2)sp(commit同步耗时,apply异步大数据频繁读取耗时)、图片加载、嵌套布局、复杂布局、动态计算也有可能、线程切换、加锁卡顿、解析大型数据、AB开关、注册kmp、短时间生成大量对象

2)解决方法:

不同问题有不同的解决方式,一般是放子线程/异步调用,动画铺平,框架解,mkkv减少一次内存切换,减少页面布局层级或者xml的布局写到java代码里面放首帧后,有一点点耗时但是不是很耗时的更改调用时机放首帧后,

3)之前遇到过:

主线程new file。

低端车机煲机多媒体应用8小时,视频播放卡顿严重,音视频不同步。一个是性能带不起来,一个是时间戳的差异越来越大

网络请求(弱网)

应用启动的时候切线程去用bitmapfactory加载了一次image,导致冷启时用户肉眼可见的空白了几百毫秒,还是高端机。其实这里放主线程比较好,因为这里不是大图加载,切线程对用户的影响可能更大一些。

AB开关,组件化的时候在类似于应用启动时高性能静态加载资源的地方使用了AB开关

下游业务使用kmp,注册下游业务的时候启动耗时又增加了,拎到首帧后线上又出来anr,因为kmp链接到了tecal

给xml里面多加了一层布局,首帧耗时增加几十ms。。。

为了实现复杂动画,动态计算css,导致安卓大部分中/低端机动画掉帧非常严重。公司的跨端组件跑不起来复杂动画。为什么要动态计算而不是帧动画?当然是动画太复杂只能根据业务规则来计算。。。

4)跟手滑的时候使用translateX,整个页面掉帧。。。

日志打印,一些常调用遍历调用的地方加日志多了容易卡。

4)首帧耗时

检测时机:

application.attachBaseContext -》 onGlobalLayout

application.attachBaseContext:Application 初始化起点,此时还没加载 Application 代码,这会儿可以插件化/热修复初始化、MultiDex 加载的起点。有的想跳过启动页直接进二级页面的可以在这里做操作。

冷启动优化点:

冷启阶段对任务进行编排,比如一些web进程、push进程不着急使用就可以到启动之后再拉起或者懒加载

如果性能监控埋点啥的可以在首帧前,业务侧非首页上的可见内容懒加载,创建时分为首帧前后,首帧前属于预加载,必须要提前执行,其他的放后面,耗时的放子线程里面

加个闪屏页提升用户体感

打开厂商定制的冷启动加速黑科技

6.Fps

每秒生成的帧数。一般是60。帧耗时 > 16ms 就会掉帧,掉帧率 = (丢帧数 / 总帧数)。线上监控通常用 帧耗时 P99 分位值 而不是平均 FPS,因为平均 FPS 会掩盖偶发长耗时。

1)一般是大列表有这个问题,解决方法

滑动时列表停止add 数据,等列表滑动停止再add数据

分页加载、懒加载、差分更新

2)背地里偷偷做微耗时操作,或者每个item去做微耗时操作。微耗时参考耗时操作

3)频繁生成对象,gc

4)频繁重绘ondraw,这个会引起屏幕频繁刷新,每次都是软件到硬件的一次刷新

5)内存泄露、越来越严重、gpu带不起来

6)view层级太多。滑动时超重,如translateX啥的。view层级太多也会导致首帧耗时慢的问题。

7)动画掉帧,改大fps

7.卡顿问题排查及原理

1)表现

滑动卡顿、动画卡顿、点击响应慢、拖拽响应慢。如果超级卡就anr闪退了

2)原因

就是刚刚那些耗时操作做在了主线程或者后台线程把资源占完了,导致前台无法及时绘制渲染。比如:绘制次数太多点击事件就在MessageQueue 里排队,导致“点击没反应”。

3)原理

Android 系统每 16ms 需要刷新一帧(60fps),如果主线程的处理时间超过 16ms,这一帧就会被丢弃,用户就会感到“掉帧”。如果丢帧严重,就成了“卡顿”。

4)绘制渲染

performdraw->翻译成displaylist指令->renderthread调用skio/vuncal/opengl等GPU绘制->然后将renderbuffer给surfacefing,然后,Choreographer根据 VSync 信号调度 doTraversal,保证绘制节奏,配合屏幕刷新节奏。

这个可以看我启动原理那篇文章里面的渲染部分。

卡顿就是帧率<  刷新率

帧率 (FPS)

每秒生成的帧数。

刷新率 (Hz)

每秒刷新的次数。

5)排查方式

看代码里面有没有耗时操作、内存正不正常。Systrace / Perfetto、Choreographer#FrameCallback、BlockCanary。

应用层(代码逻辑):  BlockCanary能直接告诉哪一行代码(如 DB操作、IO操作、复杂计算)占用了主线程太久。

渲染层(绘图指令): 使用 Layout Inspector 或 Profile GPU Rendering (GPU 呈现模式分析)。看颜色: 如果柱状图里的**红色(Draw)过高,说明 onDraw 逻辑太重;如果紫色(Layout)**过高,说明布局嵌套太深,需要用 <include>、<merge> 或者 ConstraintLayout 拍平布局。

系统层(资源竞争): 使用 Systrace / Perfetto。这是排查“为什么卡顿”的终极工具。你可以看到 Choreographer 的每一帧绘制信号,如果看到 doFrame 后面紧跟着大量的 GC 或者 Binder 调用,你就知道是 CPU 资源被抢占了。

8.anr是啥?原理是啥?

ANR(application not responding),当主线程在特定时间内没有处理完系统发送的事件,就闪退

  • activity等界面点击/滑动事件5秒没有响应

  • BroadcastReceiver: 前台 10 秒,后台 60 秒内未处理完 onReceive

  • Service: 前台 20 秒,后台 200 秒内未处理完 onCreate 等生命周期

1)排查手段

看trace、看log、看cpu是否负载过高

看 traces.txt 中的线程状态。一般是main主线程的线程状态

  • RUNNABLE:正在运行,说明代码逻辑有问题,可能在做耗时操作。大概率是 CPU 耗时计算/IO

  • BLOCKED → 在等锁,找 waiting to lock,重点看是谁持有了锁(查找 held by 关键词)。

  • WAITING /TIMED_WAITING / BLOCKED:被其他线程卡住了。在等某个操作完成,比如 Object.wait() 或 sleep

查看 logcat 中的 ANR 信息,搜索关键词 ANR in,可以看到:

  • Reason: 系统提示是因为什么原因(例如:keyDispatchingTimedOut)。

  • PID/TID: 发生 ANR 的进程和线程 ID。

看 CPU 是否负载过高

  • 如果是 CPU 满载:说明代码在做极其复杂的计算(如循环加密、大型 JSON 解析),或者逻辑陷入死循环。

  • 如果是 CPU 空闲但主线程依然在 Wait:说明主线程在等待锁(Deadlock)或者等待 IO 操作完成。

9.包大小

1)影响

互联网app:新app是影响app上架需要短时间压缩包体积,如果是很多年的老app包里面有很多功能包大小已经超了需要严格控制需求迭代的新增的包体积,不然发不了版。

车载、厂商内置app:一般这类是将app作为内置应用,包体积太大,会影响内部功能使用,流畅度啥的,如果是低端机型那就有得搞了,因为一般低端机性能非常差,内存又非常小,用一段时间很容易内存就满了。

2)怎么优化?

在不影响用户体验的情况下进行优化,压缩。

  1. 图片、资源压缩。图片可以使用ImageOptim压缩、安卓使用webp压缩、内置lottie资源找视觉压缩,基本能压很多。

  2. 代码绘制样式图片、动效。以便减少内置图片大小和减少图片加载时的空白时间。

  3. 资源改成线上资源。画不出来再尝试线上资源,图片、lottie、字体、视频

  4. 压缩xml文件。

  5. 删除无用/已下线功能代码

  6. 优化日志打印

  7. 代码裁剪。裁剪一些so库的代码只留有用到的资源,比如:ffmpeg。一些app没那么多动态化需求就没有必要引入跨端组件或者小程序的一些库了。

  8. 打开混淆

10.保活

双进程守护、service守护过时

一般是这个:前台服务的 Notification。

11.线上监控体系

  • 线下:LeakCanary + BlockCanary + Profile

  • 线上:KOOM + 自定义ANR监控(WatchDog)+ 卡顿监控(FrameMetrics)+ 日志上报

  • 兜底:接入Bugly/火线等平台兜底崩溃和ANR

兼容性问题、多语言适配、RTL

一般是互联网app 机型兼容性问题。

安卓原生就是多种安卓机型,不同安卓os版本和不同厂商之间,这种一般跟fwk和硬件有关。主要是分辨率和系统bug。

字体粗细、分辨率适配(宽、窄、大屏、2折叠、3折叠、上下折叠)、繁体字逗号自动居中、view倾斜后有锯齿、不同机型屏幕展示的字体不一样、同一个数值但是view字体大小表现不一致、时不时带出个导航栏、白边

如果是跨端就是在安卓兼容性问题上叠加不同类型的os之间,比如安卓/ios/鸿蒙之间。

iOS os 16以下识别链接时汉子识别出来是黑块、有的表情符安卓展示不出来显示一个方块。

更多的是双端差异和跨端容器实现不一致。lottie不设置宽度的时候双端表现不一致,一个填充所有,一个按照lottie素材内部宽度来展示。两帧动画衔接处有差异。

ios不保留第一帧的状态,第2帧开始前会回到初始坐标,需要marginTop来矫正。

安卓保留最后一帧状态,但是第2帧的初始坐标是第一帧的坐标,安卓需要在动画里translateY来矫正

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值