Android性能优化

144 阅读15分钟

目录

  • 1、内存优化
  • 2、ui优化
  • 3、网络优化
  • 4、启动优化

1、内存优化

1.1、解决所有的内存泄漏

1.1.1、内存泄漏:

堆上分配的对象已经不会再使用,但是GC收集器无法对其进行回收,此对象被强应用所引用 。

1.1.2、GC收集器原理:

可达性算法:从GCRoot对象为起点,向下搜索,可到达的对象是称为GC可达,GC收集器不会回收,不可到达的对象称为不GC不可达,是GC收集器回收的对象。

GCRoot对象: (1)、虚拟机栈(栈帧找那个的局部变量表)中的对象; (2)、方法区中类静态变量引用的对象; (3)、方法区中常量引用的对象。

1.1.3、内存泄漏检测方案

LeakCanary原理:

  1. LeakCanary在一个Fragment或者Activity onDestory的时候,创建一个弱引用 KeyedWeakReference 到要被监控的对象。

  2. 然后在后台线程检查引用是否被清除,如果没有,调用GC。

  3. 如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。

  4. 在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。

  5. 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。

  6. HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。

简单版:
  • 1、LeakCanary在一个Fragment或者Activity onDestory的时候,创建一个弱引用到要被监控的对象。
  • 2、然后在后台线程检查引用是否被清除,如果没有,调用GC。
  • 3、如果引用还是未被清除,把堆 内存 取出来,在另外一个进程中使用一个haha的库进行分析,确定是否内存泄漏,如果泄漏就建立泄漏对象到 GC roots 的最短强引用路径并输出。
1.1.4、常见的内存泄漏实例

(1)、单例造成的内存泄漏 单例造成内存泄漏 单例类的实例是静态的,如果需要其构造方法需要传递一个context,如果传入的是 Activity 的 Context,当传入 Activity 就会造成泄漏了。 解决方法:this.context = context.getApplicationContext();// 使用Application 的context。

(2)、内部类造成的内存泄漏 内部类创建静态对象导致内存泄漏 在Activity中有一个内部类,如果创建了这个内部类的静态对象,或者这个内部类被一个静态对象所引用,Activity关闭的时候,由于内部类会持有外部类的引用,内部类静态对象会持有外部类Activity的引用,导致Activity发生内存泄漏。 解决方法:将该内部类设为静态内部类,如果该内部类需要持有外部类的引用,则使用软引用/弱引用,在使用外部类的引用之前需进行空判断。

(3)、异步线程造成的内存泄漏 异步线程造成内存泄漏 在Activity中有一个开启一个线程执行一个runnable,这个runnable是内部类就会持有外部类Activity的引用,如果线程的生命周期比Activity的生命周期长,就不会导致Activity内存泄漏。 解决方法:同上

(4)、Handler 造成的内存泄漏 handler造成内存泄漏 在Activity中声明一个内部类handler,当使用这个handler发送一个延迟消息时,此消息执行前,Activity关闭会造成内存泄漏。 解决方法:同上

(5)资源未关闭造成内存泄漏 对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

造成内存泄漏的总结:

造成内存泄漏的的本质是长生命周期的对象引用短生命中期的对象。 比如说 1、静态变量引用一个activity,最常见的就是单例模式造成的内存泄漏; 2、另外内部类也是造成内存泄漏的一个常见的点,内部类会持有外部类的引用,activity中声明一个内部类,如果这个内部类被声明成一个静态变量或者被一个静态变量持有,那么activity关闭的时候就会发生内存泄漏。 3、内部类造成的内存泄漏还有一些其他情况,比如说,activity中声明一个runnable或者handler的内部类,如果这个runnable的生命周期比activity长,或者handler发送的延时消息的生命周期比activity的生命周期长,那么就会造成内存泄漏。 4、另外我遇到一个比较特殊的是webview的内存泄漏,webview在attach的时候会(使用mcontext,对应activity)注册一个mComponentCallbacks的回调,(以便监听配置改变或者系统剩余内存较低),在Detach的时候会反注册,反注册的前提是webviwe没有destory,所有我们在调用webviwe的destory方法前要先将webview从其父容器移除。

(高版本在webviwe没有destory方法中会先检查onDetachedFromWindow方法是否执行,没有就先执行onDetachedFromWindow方法。)

Activity onPause -> onStop -> onDestroy ----> View调用onDetachedFromWindow方法

public interface ComponentCallbacks { void onConfigurationChanged(Configuration newConfig); void onLowMemory(); }

5、另外资源未关闭也是造成内存泄漏的一个原因,比如说bitmap资源,steam资源等。

1.2、避免内存抖动

内存抖动代表频繁GC,会占用程序执行时间,造成页面卡顿等性能问题。

1.2.1、内存抖动检测方法

(1)、使用memory profiler 进行观察,如果发下内存剧增剧减则是GC时间,可查看GC时间段的内存,找出重复创建的大量对象,进行优化(例子:在recycleview中获取距离屏幕的距离时,创建大量对象)。

1.2.2、避免内存抖动的方式:

(1)、避免在onDraw中创建Paint、Bitmap对象等; (2)、避免在循环中创建临时对象; (3)、避免在scrollListener中创建对象。

1.3、图片优化

1.3.1 一些结论

1,memorySize ≈ 分辨率* 每个像素需要的字节数

2,像素点大小就几种情况:ARGB_8888(4B)、RGB_565(2B) 等等。

3,jpg、png 只是图片的容器,图片文件本身的大小与它所占用的内存大小没有什么关系。 使用Tinypng深度压缩,使用web格式代替png、jpeg格式。都不能减少内存,只能缩减包大小或者提升网络传输速度,网络图片:使用网络裁剪服务,获取适当的图片加载。

4,图片来源是 res 内的不同资源目录时,系统会根据设备当前的 dpi 值以及资源目录所对应的 dpi 值,做一次分辨率转换,规则如下:新分辨率 = 原图横向分辨率 * (设备的 dpi / 目录对应的 dpi ) * 原图纵向分辨率 * (设备的 dpi / 目录对应的 dpi )。

5,当使用 Glide 时,如果有设置图片显示的控件,那么会自动按照控件的大小,降低图片的分辨率加载。图片来源是 res 的分辨率转换规则对它也无效。

1.3.2 优化策略
  • (1)、降低分辨率

  • 降低分辨率一般使用 Glide去加载图片就好了,它会按照控件大小降低图片分辨率。其内部也是使用了inSampleSize。

  • (2)降低图片像素点大小,没有透明通道,可使用RGB_565(2B)

  • (3)使用Vector Drawable(适用于图标、线条、简单图形,不适合照片类图片, 动态缩放清晰(适配任意密度))

  • (4)drawable适配 在做图片的兼容性时,如果只想使用一张图片,则应使用3倍甚至4倍的图片(3倍是主流机型,但在4倍手机上会被放大,图片可能失真),这样在低分辨率的手机上,不仅显示清晰,而且系统会自动进行缩放,从而确保占用较小的内存。

  • (5) 配合onTrimMemory或者onLowMemory,在系统内存紧张的时候使用glide清理图片缓存

Vector Drawable内存原理 🧩 1. 资源解析阶段(不占用 Bitmap 内存) 当你调用: val drawable = AppCompatResources.getDrawable(context, R.drawable.ic_example) 系统会从 res/drawable/ic_example.xml 读取 XML 内容;

这时仅仅是 将 XML 转换为 VectorDrawable 对象的 Java/Kotlin 内部表示;

内存中只保存:

path 数据(如 M 0,0 L 10,10)

paint/stroke/alpha 等属性

✅ 这一步内存非常小,不涉及任何像素图内存。

  1. 绘制阶段(onDraw → 渲染为像素) 当这个 Drawable 被放入一个 ImageView 中,系统开始调用 onDraw(),流程如下:

Android 渲染逻辑: 系统调用 VectorDrawable.draw(Canvas);

Canvas 对象代表屏幕的某一块像素区域;

GPU 或 CPU 开始解析路径(Path)、变换(Matrix)、颜色(Paint);

将图形渲染成目标像素(比如 48x48)并显示在屏幕上。

✅ 这一步才会真正占用图像像素内存(由 Skia 引擎或 GPU 处理); ❗但注意:仅会渲染 ImageView 实际尺寸所需的像素,而不是像 PNG 那样整张图片全加载。 会根据实际显示尺寸实时绘制,不会失真、不会模糊。

www.jianshu.com/p/1f85cdb56…

1.3.2 inSampleSize
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {
    // 先将inJustDecodeBounds设置为true来获取图片的长宽属性
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // 计算inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // 加载压缩版图片
    options.inJustDecodeBounds = false;
    // 根据具体情况选择具体的解码方法
    return BitmapFactory.decodeResource(res, resId, options);
}

1、定义一个BitmapFactory.Options的一个变量,然后把这个变量的inJustDecodeBounds属性设置为true,去加载图片,这个时候不会真正去加载图片,但是可以获取到图片的宽高; 2、然后结合我们需要的宽高,计算一个采样率; 3、最后将inJustDecodeBounds设置为false,按照采样率去真正的加载图片。

1.4、内存对象的重复利用

  • 1)资源复用:通用的字符串、颜色定义、简单页面布局的复用。
  • 2)视图复用:ListView/RecycleView替代scrollview。
  • 3)Bitmap对象的复用,www.jianshu.com/p/45fbef7a5…
  • 4)使用memory profiler 去检测内存中是否可重复利用的对象; (使用memory profiler 观察内存的使用情况,在内存上升和抖动的时间段可能会有大量重复对象的创建,可取出对应时间段的内存进行查找是否有大量重复对象的创建,并可结合代码分析是否可是重复利用这些对象)

1.5、使用优化的数据结构(内存和速度平衡)

1.6、线程池管理线程

1.7、多进程,比如拍摄模块

2、ui优化

分析工具:开发者模式->显示布局边界

2.1、分析布局,减少布局嵌套或者替换消耗性能少的布局(FrameLayout,constraintlayout)

2.2、使用include+merge减少布局层级

2.3、使用viewstub提供按需加载

2.4、复用系统自带的资源 winow background

优化 RecyclerView 是 Android 开发中非常重要的一环,尤其当你在做复杂列表或大数据量场景时。以下是从多个角度系统总结的

RecyclerView 性能优化方案


✅ 一、布局优化

1. 使用合适的 LayoutManager

  • LinearLayoutManager:用于线性布局,性能较好。
  • GridLayoutManager:网格布局,适用于图片墙等。
  • 避免使用 NestedScrollView + RecyclerView 嵌套,会造成重复测量。

2. 限制布局嵌套层级

  • XML 层级越深,onMeasure/onLayout/onDraw 过程越耗时。
  • 使用 ConstraintLayout合并布局 (<merge>) 减少层级。

3. 固定 item 大小

recyclerView.setHasFixedSize(true)

当 item 大小时固定,系统可以跳过不必要的测量。

setHasFixedSize(true) 并不关心「item 的高度是否一致」,而是关心「RecyclerView 的自身尺寸是否会随着 item 内容变化而变化」。

既然你的 RecyclerView 高度是固定的,它在屏幕上始终占据同样的空间,无论里面的 item 是高是矮,它自己都不会变高或变低。那就符合 hasFixedSize = true 的使用条件。


❗不能用的情况(反例):

如果 RecyclerView 是 wrap_content,而且其中 item 高度不同或 item 数量会变,那 RecyclerView 的整体高度就会随之变化,就不能用 setHasFixedSize(true)


✅ 二、复用机制优化(缓存层面)

1. 设置合适的缓存大小

recyclerView.setItemViewCacheSize(5)

缓存更多的 ViewHolder,减少创建开销。

2. 自定义 RecycledViewPool(跨 RecyclerView 复用 View)

val pool = RecyclerView.RecycledViewPool()
pool.setMaxRecycledViews(viewType, 10)
recyclerView.setRecycledViewPool(pool)

3. 使用 DiffUtil 替代 notifyDataSetChanged()

  • 能精准更新变化的 item,避免整页刷新。
  • 性能大幅提升。
DiffUtil.calculateDiff(diffCallback).dispatchUpdatesTo(adapter)

2. 图片加载优化

  • 使用 Glide/Picasso/Fresco 等异步图片加载库,开启缓存、缩略图预处理等。
  • 禁止硬解码大图导致 OOM。

✅ 四、滑动性能优化

1. 开启 RecyclerView 的硬件加速(默认是开启的)

android:hardwareAccelerated="true"

2. 监听滑动状态,延迟一些操作(如加载大图)

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            // 加载图
        }
    }
})

✅ 六、视图更新优化

1. 使用 payload 进行局部刷新

adapter.notifyItemChanged(position, "payload")

然后在:

onBindViewHolder(holder, position, payloads)

中只更新变动部分视图。


✅ 七、预加载与分页优化(大数据列表)

1. 使用分页库 Paging 3(官方推荐)

  • 异步分页加载,流式处理
  • 自动处理 DiffUtil、列表状态

2. 手动预加载下一页数据

  • 滑动到最后几个 item 时,加载下一页。

✅ 五、避免内存泄漏和 GC

1. ViewHolder 中不引用 Activity/Fragment

  • 会导致内存泄漏,避免写成 val context = itemView.context as MainActivity

2. 避免频繁触发 GC

  • 列表复用中,避免 new 对象、避免频繁创建临时变量。

✅ 三、数据绑定优化

1. 避免在 onBindViewHolder 中执行耗时操作

  • 不要加载网络图、执行数据库查询、计算复杂字符串等。
  • 可以用 ViewModel 或后台线程提前准备数据。

✅ 八、动画优化

1. 移除默认动画或自定义动画减少跳动感

(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false

🧠 加分项:你可以使用 RecyclerView Performance Profiler 工具 来分析是否存在重复绑定、丢帧等问题。


🚀 最佳实践组合(总结):

kotlin
复制编辑
recyclerView.apply {
    layoutManager = LinearLayoutManager(context)
    setHasFixedSize(true)
    setItemViewCacheSize(5)
    itemAnimator = null
    recycledViewPool.setMaxRecycledViews(0, 10)
}
  • 配合 DiffUtil + payload + 限制布局层级 + 缓存 ViewHolder,可让 RecyclerView 实现极限丝滑!

3、网络优化

优化工具:network-inspector

打点建立线上监控体系

OkHttp 的 EventListener 能获取到请求的各个步骤的耗时和数据,提供了DNS 解析建立连接建立 HTTPS 连接写入请求首部写入请求体等事件的开始结束失败的回调方法。而且 EventListener 还能和 Glide 搭配使用,以获取图片请求的各个步骤的数据。

3.1、网络速度:

正常一条网络请求需要经过的流程是这样: (1)、DNS 解析,请求DNS服务器,获取域名对应的 IP 地址; (2)、与服务端建立连接,包括 tcp 三次握手,安全协议同步流程; (3)、连接建立完成,发送和接收数据,解码数据。

这里有明显的三个优化点:

3.1、IP直连或者HTTPDNS;
3.2、开启 keep-alive进行连接复用;
3.3、减少请求和返回数据的大小;

(1)、请求和返回数据做Gzip压缩; (2)、精简数据格式; (3)、图片:使用webp图片格式代替png、jpeg;按需加载图片(图片裁剪,也可按网络状态返回不同分辨率的图片);

3.4、数据缓存(h5和na共享作品数据,视频缓存);

http强制缓存和对比缓存

3.5、协议优化;(如使用优化的的更好的http2,请求头压缩)

在 HTTP/1 中,每次请求都会建立一次HTTP连接,也就是我们常说的3次握手4次挥手,这个过程在一次请求过程中占用了相当长的时间,即使开启了 Keep-Alive ,解决了多次连接的问题,但是依然有两个效率上的问题:

第一个:串行的文件传输。当请求a文件时,b文件只能等待,等待a连接到服务器、服务器处理文件、服务器返回文件,这三个步骤。我们假设这三步用时都是1秒,那么a文件用时为3秒,b文件传输完成用时为6秒,依此类推。(注:此项计算有一个前提条件,就是浏览器和服务器是单通道传输) 第二个:连接数过多。我们假设Apache设置了最大并发数为300,因为浏览器限制,浏览器发起的最大请求数为6,也就是服务器能承载的最高并发为50,当第51个人访问时,就需要等待前面某个请求处理完成。 HTTP/2的多路复用就是为了解决上述的两个性能问题。 在 HTTP/2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。 帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。 多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。

对请求数据进行Gzip压缩

 static class GzipRequestInterceptor implements Interceptor {
  @Override public Response intercept(Chain chain) throws IOException {
      Request originalRequest = chain.request();
      if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
        return chain.proceed(originalRequest);
      }

      Request compressedRequest = originalRequest.newBuilder()
          .header("Content-Encoding", "gzip")
          .method(originalRequest.method(), gzip(originalRequest.body()))
          .build();
      return chain.proceed(compressedRequest);
    }

    private RequestBody gzip(final RequestBody body) {
      return new RequestBody() {
        @Override public MediaType contentType() {
          return body.contentType();
        }

        @Override public long contentLength() {
          return -1; // 无法知道压缩后的数据大小
        }

        @Override public void writeTo(BufferedSink sink) throws IOException {
          BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
          body.writeTo(gzipSink);
          gzipSink.close();
        }
      };
    }
  }
  

网络优化补充:

3.6、断点续传。
3.7、请求打包;(如埋点统计)

bugly监控工具

bugly.tds.qq.com/docs/tutori…

5.参考 1,juejin.cn/post/690148… 2,juejin.cn/post/684490… 3,www.jianshu.com/p/a9d861732… 4,juejin.cn/post/684490… 5,www.jianshu.com/p/9c897b418… 6,启动优化,juejin.cn/post/684490… 7,www.cnblogs.com/yongdaimi/p…