文章目录

  • 性能优化:
  • 工具:
  • memory profiler
  • LeakCanary
  • arthook
  • epic 库
  • java内存管理机制
  • java 内存回收机制
  • Android内存管理机制
  • Dalvik与 Art区别
  • Low Memory Killer
  • 内存抖动解决
  • 内存泄漏解决
  • 第一个内存泄漏点
  • 内存很大的bitmap
  • 第一个地方 生成二维码的时候每隔一定时间会动态创建二维码
  • 解决方案:
  • 如何定位问题
  • native 内存一直在增加 分析
  • 发现个问题 android studio 插线后使用profile内存自动升高, 感觉是android studio 的bug
  • Bitmap内存模型
  • bitmap 优化
  • 内存优化细节
  • 优化结果:


性能优化:

工具:

memory profiler

android studio 自带的. 找不到profile 的话 顶部导航栏

  1. 点击下载样式的按钮可以找到当前的内存

Android DDR优化 android内存优化工具_性能优化 内存优化

这样可以看到当前内存是哪里消耗的最多,还有一些其他的内存信息, 最主要看一个bitmap

LeakCanary

https://square.github.io/leakcanary/getting_started/

大致原理

监听Activity生命周期->onDestroy以后延迟5秒判断Activity有没有被回收->如果没有回收,调用GC,再此判断是否回收,如果还没回收,则内存泄露了,反之,没有泄露。

引入:

dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'
}

arthook

epic 库

支持ART上的Java方法HOOK

java内存管理机制

  1. 方法区: 存储 java静态变量 ,常量.这块区域所有线程都共享.和Java堆一样也是各个线程共享的内存区域,他存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。。
  2. 虚拟机栈: Java虚拟机栈 .用于存储局部变量、操作数、操作数栈、动态链接、方法出口等信息. 局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress类型。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
  3. 本地方法栈: :与虚拟机栈类似,他们之间的区别是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
  4. 堆: Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域。在此内存区域中唯一目的就是存放对象实例,几乎所有的对象都在这里分配内存。
  5. 程序计数器:是一块较小内存,可以看作是当前线程所执行的字节码的行号指示器。每条线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响。
    比如当前程序执行到第几行.

java 内存回收机制

(1).标记-清除算法:

最基础的垃圾收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。

标记-清除算法的缺点有两个:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

(2).复制算法:

将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

复制算法的缺点显而易见,可使用的内存降为原来一半。

(3).标记-整理算法:

标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。

标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。

复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高。

(4).分代收集算法:

根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。

  • 结合多种收集算法的优势
  • 新生代对象存活率低,复制算法
  • 老年代对象存活率高,标记-整理算法.

Android内存管理机制

  • 内存弹性分配,分配值与最大值受具体设备影响
  • OOM场景: 内存真正不足. 可用内存不足

Dalvik与 Art区别

Dalvik是Google公司自己设计用于Android平台的虚拟机
ART代表AndroidRuntime 应用安装的时候就预编译字节码到机器语言,这一机制叫Ahead-Of-Time(AOT)预编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快

  • Dalvik仅固定一种回收算法,没有改变
  • 5.0以后用art虚拟机.art回收算法可运行期间选择(在不同情况下选择不同垃圾回收机制-) 比如当前应用在前台,对用户来说响应速度最重要.那么就选择最简单的算法: 标记清除算法, 当应用界面在后台.就可以选择使用标记-整理算法补充

Low Memory Killer

  • 进程分类,有优先级.优先回收低优先级的进程
  • 回收收益

内存抖动解决

  • 定义: 内存频繁废品和回收导致内存不稳定
  • 表现: 频繁GC. 内存曲线呈锯齿状
  • 危害: 导致卡顿 , OOM
  • 使用Memor Profiler初步排查
  • 使用Memor Profiler 或cpu Profile结合代码排查

内存泄漏解决

内存抖动的优化

尽量避免在循环体或者频繁调用的函数内创建对象,应该把对象创建移到循环体外。总之就是尽量避免频繁GC。

项目中出现的问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jZdUgVJa-1617777245772)(https://liudao01.github.io/picture/img/686db9de-e141-44d9-ab7f-67de6aa2b134.png)]

第一个内存泄漏点

拉开看

Android DDR优化 android内存优化工具_性能优化 内存优化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VfQPra93-1617777245775)(https://github.com/liudao01/picture/img/20210202215043.png)]

接着我们点击Allocations进行对象分配数量排序,之所以点击这个是因为一般在循环,频繁调用的地方可能发生内存抖动

排名第一的是String ,
Cleaner是垃圾回收相关的对象,NativeAllocationRegistry是内存分配相关的额对象,我们查看其调用栈如图:
我一看咋还有百度的. 我进百度工具类N多个循环在打印信息.难怪内存抖动. 如下

StringBuffer sb = new StringBuffer(256);
//                sb.append("time : ");
//                /**
//                 * 时间也可以使用systemClock.elapsedRealtime()方法 获取的是自从开机以来,每次回调的时间;
//                 * location.getTime() 是指服务端出本次结果的时间,如果位置不发生变化,则时间不变
//                 */
//                sb.append(location.getTime());
//                sb.append("\nlocType : ");// 定位类型
//                sb.append(location.getLocType());
//                sb.append("\nlocType description : ");// *****对应的定位类型说明*****
//                sb.append(location.getLocTypeDescription());
//                sb.append("\nlatitude : ");// 纬度
//                sb.append(location.getLatitude());
//                sb.append("\nlongtitude : ");// 经度
//                sb.append(location.getLongitude());
//                sb.append("\nradius : ");// 半径
//                sb.append(location.getRadius());
//                sb.append("\nCountryCode : ");// 国家码
//                sb.append(location.getCountryCode());
//                sb.append("\nProvince : ");// 获取省份
//                sb.append(location.getProvince());
//                sb.append("\nCountry : ");// 国家名称
//                sb.append(location.getCountry());
//                sb.append("\ncitycode : ");// 城市编码
//                sb.append(location.getCityCode());
//                sb.append("\ncity : ");// 城市
//                sb.append(location.getCity());
//                sb.append("\nDistrict : ");// 区
//                sb.append(location.getDistrict());
//                sb.append("\nTown : ");// 获取镇信息
//                sb.append(location.getTown());
//                sb.append("\nStreet : ");// 街道
//                sb.append(location.getStreet());
//                sb.append("\naddr : ");// 地址信息
//                sb.append(location.getAddrStr());
//                sb.append("\nStreetNumber : ");// 获取街道号码
//                sb.append(location.getStreetNumber());
//                sb.append("\nUserIndoorState: ");// *****返回用户室内外判断结果*****
//                sb.append(location.getUserIndoorState());
//                sb.append("\nDirection(not all devices have value): ");
//                sb.append(location.getDirection());// 方向
//                sb.append("\nlocationdescribe: ");
//                sb.append(location.getLocationDescribe());// 位置语义化信息
//                sb.append("\nPoi: ");// POI信息
//                if (location.getPoiList() != null && !location.getPoiList().isEmpty()) {
//                    for (int i = 0; i < location.getPoiList().size(); i++) {
//                        Poi poi = (Poi) location.getPoiList().get(i);
//                        sb.append("poiName:");
//                        sb.append(poi.getName() + ", ");
//                        sb.append("poiTag:");
//                        sb.append(poi.getTags() + "\n");
//                    }
//                }
//                if (location.getPoiRegion() != null) {
//                    sb.append("PoiRegion: ");// 返回定位位置相对poi的位置关系,仅在开发者设置需要POI信息时才会返回,在网络不通或无法获取时有可能返回null
//                    PoiRegion poiRegion = location.getPoiRegion();
//                    sb.append("DerectionDesc:"); // 获取POIREGION的位置关系,ex:"内"
//                    sb.append(poiRegion.getDerectionDesc() + "; ");
//                    sb.append("Name:"); // 获取POIREGION的名字字符串
//                    sb.append(poiRegion.getName() + "; ");
//                    sb.append("Tags:"); // 获取POIREGION的类型
//                    sb.append(poiRegion.getTags() + "; ");
//                    sb.append("\nSDK版本: ");
//                }
//                sb.append(mClient.getVersion()); // 获取SDK版本

我给注释后就好很多了

内存很大的bitmap

bitmap 反复创建销毁的问题

有两个地方 一个是首页轮播图的图片, 另一个是

闪电付的图片存在重复创建问题. 有两图片一个是二维码一个是条形码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XTb2N5Lx-1617777245777)(https://github.com/liudao01/picture/blob/master/img/20210202-222641.jpg)]

第一个地方 生成二维码的时候每隔一定时间会动态创建二维码

/**
     * 生成二维码
     *
     * @param content
     */
    private void createQRCode(String content) {
        //生成二维码相关放在子线程里面
        ThreadManager.getDownloadPool().execute(() -> {
            

            qrCodeBitmap = CodeUtils.createQRCode(content, 540, null);
            if (activity != null) {
                activity.runOnUiThread(() -> {
                    //显示二维码ivQrCode
//                    final int byteCount = qrCodeBitmap.getByteCount();
//                    LogUtil.d("二维码占得内存字节 = "+byteCount);
                    ivQrCode.setImageBitmap(qrCodeBitmap);
                });
            }
        });

优化前内存消耗

Android DDR优化 android内存优化工具_Android DDR优化_03

  • 优化方案1

生成图片前先把图片清空,再用新的bitmap给他赋值

代码如下:

优化后内存消耗:

Android DDR优化 android内存优化工具_android_04

结果: 降低了将近50M 的内存 恐怖啊

  • 优化方案2

降低图片的质量:

先打印下生成的二维码所占的内存字节数

获取字节数方法:

bitmap.getByteCount
二维码占得内存字节 = 1166400
 条形码占得内存字节 = 1166400

看一下内存消耗:

两张图所占内存 大约有2M 每次返回都要重新创建.这里可以优化一下.
这里我用的是库 默认创建的bitmap是ARGB_8888 那么我可以给优化下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-251XsXrP-1617777245780)(https://liudao01.github.io/picture/img/企业微信截图_99fd3f8d-fdac-4a96-bf3c-68bad123e323.png)]

解决方案:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3XRkMStC-1617777245781)(https://liudao01.github.io/picture/img/b5fa9ab8-4af3-4b99-84bc-d1021f1ddd2e.png)]

http://xingyun.xiaojukeji.com/docs/dokit/#/androidGuide

如何定位问题

开始的内存情况

Android DDR优化 android内存优化工具_android_05

运行一段时间后的内存情况

Android DDR优化 android内存优化工具_android_06

可以明显看到 graphics 内存增加 所以明显是图片的问题.

可以分析1 从网络 2 从本地. 网络的我先把加载网络大图的地方全部注释,依旧内存不断增大.

结合我的代码可以知道 我的页面存在循环创建对象的情况.

for (int i = 0; i < size; i++) {
  
            if (i == 0) {
                rl_ecard_root.setBackgroundResource(R.mipmap.me_e_card1);
            } else if (i == 1) {
                rl_ecard_root.setBackgroundResource(R.mipmap.me_e_card2);
            } else if (i % 3 == 2) {
                rl_ecard_root.setBackgroundResource(R.mipmap.me_e_card3);
            }
  }

有这样的代码 这样每次进入返回页面都会重复创建

优化后:

native 内存一直在增加 分析

  1. dump java heap
  2. 保存memory-20210204T161535.hprof 文件
  3. mat 下载 https://www.eclipse.org/mat/downloads.php
  4. 创建java虚拟机失败了 https://www.jianshu.com/p/15597858caa0 (我通过vim改才成功)

发现个问题 android studio 插线后使用profile内存自动升高, 感觉是android studio 的bug

Bitmap内存模型

获取bitmap内存占用

  • getBytecount
  • 宽 * 高 * 一个像素所占用的内存
  • 资源目录需要再乘以一个压缩比例

常规优化方案:

  • 背景: 图片对内存优化至关重要,图片宽高大于控件宽高

bitmap 优化

Bitmap的优化策略
经过上面的分析,我们可以得出Bitmap优化的思路:

  1. BitmapConfig的配置
  2. 使用decodeFile、decodeResource、decodeStream进行解析Bitmap时,配置inDensity和inTargetDensity,两者应该相等,值可以等于屏幕像素密度*0.75f
  3. 使用inJustDecodeBounds预判断Bitmap的大小及使用inSampleSize进行压缩
  4. 对Density>240的设备进行Bitmap的适配(缩放Density)
  5. 2.3版本inNativeAlloc的使用
  6. 4.4以下版本inPurgeable、inInputShareable的使用
  7. Bitmap的回收

所以我们根据以上的思路,我们将Bitmap优化的策略总结为以下3种:

  1. 对图片质量进行压缩
  2. 对图片尺寸进行压缩
  3. 使用libjpeg.so库进行压缩

内存优化细节

  • LargeHeap 属性 开启
  • onTrimMemory 当系统内存不足会回调这个. 我们可以在这里回收一些内存供应用使用
  • 使用优化过的集合: SparseArray
  • 谨慎使用SharedPreference
  • 谨慎使用外部库

优化结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nV1XaOAE-1617777245784)(https://liudao01.github.io/picture/img/企业微信截图_96d7a599-03fa-4ca9-ac26-fd6d40916525.png)]