Android-内存优化

Android-内存优化

内存泄漏和内存抖动

内存泄漏

(1)内存泄漏的原因:

  • Handler 发送的消息未被处理则会导致 MessageQueue 一直持有 Handler 和 Message 的引用,解决办法:创建静态内部 Handler 类继承自 Handler,并实例化该内部的 Handler,并且在 onDestroy() 方法中手动移除所有未处理的消息、或者使用进程唯一的 MainLooper 来实例化 Handler。

  • 单例模式的生命周期通常和 App 一致,若其中传入了 Context,则会导致对应的 Context 无法被回收,此时可以改用 Application 的 Context。

  • 使用 View、Context 等作为参数传递到外部对象中,由于 Context 和 View 对象很容易被回收,如果被外部对象持有,很可能会导致内存泄漏。可以使用弱引用 WeakReference。

  • 非静态匿名内部类,例如一个非静态的 Runnable 对象,被用来实例化一个匿名 Thread 对象:new Thread(runnable).start();,由于该 Runnable 对象非静态,会自动持有其外部类的引用,则导致外部类无法被回收。可以通过把 Runnable 对象声明为 static 解决。

  • 注册/反注册未成对使用,例如广播接收器,可以在 onCreate()onResume() 中注册,在 onDestroy()onPause() 中释放。

  • 资源为关闭,例如 Cursor、File、Bitmap、视频等,通常内部实现了一些缓冲技术,并且可能还涉及到 Native 层,都需要手动关闭资源,否则会引起内存泄漏。

  • 集合未及时清理,例如将一些 View、Context 等存入集合中导致无法回收,或是将集合声明为 static,都会导致内存泄漏。

(2)内存泄漏的检测:

主要是引入 LeakCanary,然后在 Application 中初始化和安装,之后在设备上运行 App 即可在 LeakCanary 中自动检测。还有MAT、Lint等。

  • LeakCanary原理

  • 利用logging监听方法耗时,会不会让app增大延迟

通过Looper.setMessageLogging(Printer)来设置自己的Printer,Printer会在Looper处理一个事件的开始和结束输出Msg的相关信息。

内存抖动

内存抖动主要是短时间内有大量对象产生和销毁,伴随着频繁的 GC,占用大量 CPU 和 UI 线程资源,导致 App 卡顿。

(1)内存抖动的原因:

  • 在循环中声明和创建对象,可以改为在外部创建对象,然后在循环内实例化。

  • 频繁改变 String 对象,可以改为使用 StringBuilder 最后再转为 String

  • View 中的 onDraw() 方法会被频繁调用,应当尽量避免创建对象。

  • 频繁使用的图片资源,或者复用几率较大的对象,可以建立缓存。

(2)内存抖动的检测和定位:

使用 AndroidStudio 自带的工具 Android Profile,选择 Memory 栏,当出现 GC 时,会显示一个垃圾桶图标,如果 GC 出现的频繁,则很可能是内存抖动。

使用 AndroidStudio 自带的工具:Tools - Android - Android Device Monitor,打开 App,在 Monitor 工具中选择 App 对应的进程,然后运行对应的功能后,选择 DDMS 工具,查看一个 .trace 文件,在“main”栏会显示具体的抖动,找到发生抖动(柱状图起伏很大的一段)放大后,对应的找到相关的方法(方法被调用时,调用者的方法序号小于其调用的方法的序号),根据实际情况追溯到对应的方法中去查看有没有可能导致内存抖动的代码并修改。


性能优化

(1)逻辑层

尽可能消除或减少内存泄漏、内存抖动。

(2)UI 层

  • 减少布局层级,可通过手机的“过渡绘制”和 AndroidStudio - Tools - Layout Inspector 查看。

  • 使用 ViewStub 占位不常用资源。

  • 使用简单的布局,例如在层级相同的前提下,使用 FrameLayout 或 LinearLayout 替代 RelativeLayout。

  • 优化自定义 View 中的 onDraw(),避免复杂语句。

(3)应用层

  • 耗时操作异步处理。

  • 复用资源本地缓存。

  • Bitmap 优化,例如读取信息时,设置 BitmapFactory.Options.inJustDecodeBounds = true;,则仅读取宽高而不将具体数据读入内存,当图片过大时,对图片进行压缩,缓存一份压缩后的图片,对需要显示大图的地方,使用 BitmapRegionDecoder,指定 Bitmap 的区域进行解码。

(4)代码层

  • ListView、RecyclerView,使用 ViewHolder 复用布局。

  • 减少枚举类。由于 JVM 使用 int 作为默认整型变量,因此在数据量大和非必要场景下,使用 int 替代 short、byte 反而性能更好。

  • 使用 SparseArray 或 ArrayMap 代替简单的 HashMap 结构。

稀疏数组是针对替换 HashMap<Integer, Object> 的,其意义在于:如果一个 HashMap 里面存的数据很少,会浪费很多空闲的内存空间,因此可以使用一个压缩后的矩阵来表示。SparseArray 矩阵,分为上下两个部分,共三列。上部分只有第一行,从左到右三列分别是:原 HashMap 的行数、原 HashMap 的列数、原 HashMap 共使用了几个元素。下部分则分别记录了原 HashMap 中使用了的元素分别在原 HashMap 中的行下标、列下标、取值。

ArrayMap使用两个数组来存放键值对,一个数组存放Key,另一个存放Value。

如果Key确定是Int,可使用SparseArray,Long可使用LongSparseArray,其他使用ArrayMap,但数据量大时,还是使用HashMap,因为SparseArray和ArrayMap使用二分法,将计算后的Hash值按从小到大的顺序排列插入和读取。

(5)启动优化

利用window background快速展示一个界面给用户快速的心理预期,启动的过程,如第一个Activity的onCreate中避免繁重的初始化任务,IO、网络等耗时操作懒加载,减少布局嵌套等。

(6)APK体积优化

资源文件方面:使用webp代替传统jpg和png格式,单色背景或简单几何图形使用drawable或者Vector代替图片,大型媒体文件使用动态联网加载代替打进APK包,多Module之间复用的资源避免冗余。

代码层,去除无用的第三方依赖,减少枚举类,代码混淆,


ListView和RecyclerView性能对比和性能优化

ListView 开发成本低,性能和功能上 RecyclerView 更优。

RecyclerView在很多方面能取代ListView,Google为什么还不弃用ListView?

在某些轻量级场景下,例如纯文字的列表时,RecyclerView强制使用Holder的形式芳儿增加了开销,而且开发成本也更高一些。

(1)ListView:

Adapter.getView中,ConvertView==null时,说明当前的Item的View还没创建,LayoutInflate一个View,否则说明View可复用,直接让View=convertView。

根据每个Item都有的布局创建一个ViewHolder,这个ViewHolder保存了每一个Item通用的布局的View的对象,如果ConvertView==null,说明当前Item的View还没创建,则新建一个ViewHolder并将该Holder存入View.tag里,否则说明可复用,直接getTag取出holder,然后重新给里面的View对象赋值。

(2)RecyclerView:

onBindViewHolder运行在UI线程中,尽量避免耗时操作

数据量较大时,用DiffUtil代替notifyDataSetChanged,只刷新局部数据:
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(oldDatas, newDatas), true);
diffResult.dispatchUpdatesTo(mAdapter);
其本质是计算出新旧数据具体在哪些地方和哪一段发生了变化,然后根据实际情况调用:
mAdapter.notifyItemRangeInserted(position, count);
mAdapter.notifyItemRangeRemoved(position, count);
mAdapter.notifyItemMoved(fromPosition, toPosition);
mAdapter.notifyItemRangeChanged(position, count, payload);
来实现的。

布局优化,减少Item的过度绘制、减少Item的层级

RecyclerView的Item很灵活,因此每一次加载,都会调用requestLayout来刷新父布局,如果所有Item的高度固定,可以调用RecyclerView.setHasFixedSize(true);避免

如果RecyclerView嵌套了RecyclerView,且可以使用相同的Adapter,则可以设置RecyclerView.setRecycledViewPool(pool)来共用一个RecycledViewPool,如果LayoutManager是LinearLayoutManager或其子类,需要手动开启:layout.setRecycleChildrenOnDetach(true)


Bitmap加载大图(加载局部)

https://www.jianshu.com/p/73aecb2b85e6

(1)设置 inJustDecodeBounds 来预检测图片大小。

(2)大图尝试先压缩显示预览图。

(3)当放大显示时,测量当前 ImageView 实际可显示的范围,按照实际可显示范围加载局部图(BitmapRegionDecoder)。

(4)BitmapRegionDecoder.newInstance(InputStream, boolean),传入图片输入流实例化一个 bitmapRegionDecoder

(5)编码局部:bitmapRegionDecoder.decodeRegion(Rect, BitmapFactory.Options);

(6)绘制:canvas.drawBitmap()

(7)通过手势监听类 GestureDetector 的实例对象在 onTouch(View v, MotionEvent event) 中调用 gestureDetector.onTouchEvent(event),并继承重写 GestureDetector.onScroll(motionEvent e1, MotionEvent e2, float distanceX, float distanceY) 来监听手势变化,并重新计算需要显示的矩形范围,然后在 onDraw() 中重新绘制对应区域的 Bitmap。