part 3 App内存优化

Part 3 App内存优化

一 内存优化介绍及工具选择

1、内存优化介绍

内存问题

内存抖动:锯齿状、GC导致的卡顿
内存泄漏:可用内存减少、频繁GC
内存溢出:OOM、程序异常

2、工具选择

Memory Profiler

实时图表展示应用内存使用情况
识别内存泄漏、抖动等
提供捕获堆转储、强制GC以及跟踪内存分配的能力
总结:方便直观、线下平时使用

Memory Analyzer(MAT)

强大的java heap分析工具、查找内存泄漏及内存占用
生成整体报告、分析问题等
线下深入使用

LeakCanary

自动内存泄漏检测
线下集成

二 Android内存管理机制

1、java内存管理机制

java的内存分配:

方法区(存放java的类信息,常量,静态变量 等,是所有线程共享的)
虚拟机栈(局部变量表和操作数栈等,为java方法调用服务的,线程私有)
本地方法栈(为native方法服务的,线程私有)
堆(所有线程共享,真正的进行内存分配,GC主要作用的区域)
程序计数器(存储当前线程到多少行)

java的内存回收算法:

在这里插入图片描述

标记-清除算法总结:标记清除算法:标记处所有需要回收的对象,同一回收所有被标记的对象。效率不高,产生大量不连续的碎片(内存空洞)

在这里插入图片描述

复制算法总结:复制算法:将内存划分为大小相等的两块,一块内存用完之后复制存活的对象到另一块,清理另一块内存。实现简单,运行高效,浪费一半内存,代价大

在这里插入图片描述

标记-清理总结:标记整理算法:标记过程与标记清除算法一样,存活对象往一端进行移动,清理其余内存。避免标记清除导致的内存碎片,避免复制算法的控件浪费。

分代收集算法:

集合多种收集算法的优势,新生代对象存活率低(复制算法),老年代对象存活率高(标记整理)
Android的内存管理机制:内存弹性分配,分配值和最大值受具体设备影响。oom场景:内存真正不足,可用内存不足。
Dalvik与Art区别:Dalvik仅固定一种回收算法,Art回收算法可运行期间选择,Art具备内存整理能力,减少内存空洞。
Low Memory Killer 进行分类,回收收益。

2、Android内存管理机制

内存弹性分配。分配值和最大值受具体设备影响(如高端机和低端机的不同)

OOM场景:内存真正不足、可用内存不足(整个系统内存都不足)

Dalvik与Art区别:
Dalvik仅固定一种回收算法(运行期没法改变)
Art回收算法可运行期选择
Art具备内存整理能力,减少内存空洞

Low Memory killer
进程分类(前台-可见进程-桌面进程-服务进程-后台进程-空进程)
回收收益

三 内存抖动解决实战

1、内存抖动介绍

定义:内存频繁分配和回收导致内存不稳定
表现:频繁GC、内存曲线呈现锯齿状
危害:导致卡顿、OOM

内存抖动导致OOM的原因:频繁创建对象,导致内存不足及碎片(不连续),不连续的内存片无法被分配,导致OOM

2、内存抖动怎么解决

使用Memory Profiler(可以观察内存曲线)

在这里插入图片描述

在这里插入图片描述

内存抖动解决技巧:找循环或者频繁调用的地方

四 内存泄露解决实战

1、内存泄漏介绍

定义:内存中存在已经没有用的对象
表现:内存抖动、可用内存逐渐减少
危害:内存不足、频繁GC、OOM

2、内存泄漏怎么解决

使用Memory Analyzer
下载地址:https://www.eclipse.org/mat/downloads.php
转换:hprof-conv 原文件路径 转换后文件路径

在这里插入图片描述
在这里插入图片描述

使用MAT工具进行分析哪些对象存在泄漏(下一章介绍)

五 全面理解MAT

1、MAT的介绍

参考文档
https://www.jianshu.com/p/c8e0f8748ac0 (MAT使用进阶)
https://www.cnblogs.com/larack/p/6071209.html (内存泄漏 之 MAT工具的使用
)

六 ARTHook优雅检测不合理图片

1、Bitmap内存模型

API10之前Bitmap自身在Dalvik Heap中,像素在Native
优点:不占用java层的内存,不易出现OOM
缺点:bitmap被回收,native像素没有被回收,回收时机不确定

API10之后像素也放在Dalvik Heap中
优点:bitmap回收native像素也被及时回收
缺点:占用java层内存

API26之后像素在Native
优点:不占用java层的内存,不易出现OOM
Google解决了不能及时回收的问题

获取bitmap占用内存

getByteCount 运行时动态计算出来
一像素占用内存 直接计算结果

2、常规方式

背景

图片对内存优化至关重要、图片宽高大于控件宽高(证明图片尺寸不合理)

实现

继承ImageView,复写实现计算大小

总结:侵入性强,不通用

3、ART方式

ARTHooK介绍

挂钩,将额外的代码挂住原有的方法,修改执行逻辑

运行时插桩

性能分析

Epic介绍

Epic是一个虚拟机层面、以java Method为粒度的运行时Hook框架
支持Android 4.0-9.0机型
https://github.com/tiann/epic

使用方式:

dependencies {
compile ‘me.weishu:epic:0.3.6’
}
继承XC_MethodHook,实现相应的逻辑
注入Hook:DexposedBridge.findAndHookMethod

几个例子

1.监控Java线程的创建和销毁:
class ThreadMethodHook extends XC_MethodHook{
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Thread t = (Thread) param.thisObject;
        Log.i(TAG, "thread:" + t + ", started..");
    }

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        Thread t = (Thread) param.thisObject;
        Log.i(TAG, "thread:" + t + ", exit..");
    }
}

DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        Thread thread = (Thread) param.thisObject;
        Class<?> clazz = thread.getClass();
        if (clazz != Thread.class) {
            Log.d(TAG, "found class extend Thread:" + clazz);
            DexposedBridge.findAndHookMethod(clazz, "run", new ThreadMethodHook());
        }
        Log.d(TAG, "Thread: " + thread.getName() + " class:" + thread.getClass() +  " is created.");
    }
});
DexposedBridge.findAndHookMethod(Thread.class, "run", new ThreadMethodHook());
以上代码拦截了 Thread 类以及 Thread 类所有子类的 run方法,在 run 方法开始执行和退出的时候进行拦截,就可以知道进程内部所有Java线程创建和销毁的时机;更进一步,你可以结合Systrace等工具,来生成整个过程的执行流程图,比如:


2.监控dex文件的加载:
DexposedBridge.findAndHookMethod(DexFile.class, "loadDex", String.class, String.class, int.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        String dex = (String) param.args[0];
        String odex = (String) param.args[1];
        Log.i(TAG, "load dex, input:" + dex + ", output:" + odex);
    }
});


3.完整版的图片尺寸监控
3.1 首先实现XC_MethodHook,并实现我们的逻辑
public class ImageHook extends XC_MethodHook {

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        // 实现我们的逻辑
        ImageView imageView = (ImageView) param.thisObject;
        checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
    }

    private static void checkBitmap(Object thiz, Drawable drawable) {
        if (drawable instanceof BitmapDrawable && thiz instanceof View) {
            final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            if (bitmap != null) {
                final View view = (View) thiz;
                int width = view.getWidth();
                int height = view.getHeight();
                if (width > 0 && height > 0) {
                    // 图标宽高都大于view带下的2倍以上,则警告
                    if (bitmap.getWidth() >= (width << 1)
                            && bitmap.getHeight() >= (height << 1)) {
                        warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
                    }
                } else {
                    final Throwable stackTrace = new RuntimeException();
                    view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                        @Override
                        public boolean onPreDraw() {
                            int w = view.getWidth();
                            int h = view.getHeight();
                            if (w > 0 && h > 0) {
                                if (bitmap.getWidth() >= (w << 1)
                                        && bitmap.getHeight() >= (h << 1)) {
                                    warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
                                }
                                view.getViewTreeObserver().removeOnPreDrawListener(this);
                            }
                            return true;
                        }
                    });
                }
            }
        }
    }


    private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
        String warnInfo = new StringBuilder("Bitmap size too large: ")
                .append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
                .append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
                .append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
                .toString();
        LogUtils.i(warnInfo);
    }

}

3.2 在Application中或者Activity中初始化
DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param);
                DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
            }
        });     
        
3.3 log 信息
03-04 12:08:54.035 21103-21103/com.kaolafm.kradio.k_radio_horizontal E/zsj: Bitmap size too large: 
     real size: (1280,1424)
     desired size: (409,469)
     call stack trace: 
    java.lang.RuntimeException: Bitmap size too large
        at com.kaolafm.kradio.lib.hook.ImageHook.checkBitmap(ImageHook.java:34)
        at com.kaolafm.kradio.lib.hook.ImageHook.afterHookedMethod(ImageHook.java:20)
        at com.taobao.android.dexposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:273)
        at me.weishu.epic.art.entry.Entry.onHookVoid(Entry.java:73)
        at me.weishu.epic.art.entry.Entry.referenceBridge(Entry.java:167)
        at me.weishu.epic.art.entry.Entry.voidBridge(Entry.java:87)
        at android.support.v7.widget.AppCompatImageView.setImageBitmap(AppCompatImageView.java:107)
        at com.kaolafm.kradio.k_kaolafm.horizontal.RadioPlayerFragment.lambda$showCover$4$RadioPlayerFragment(RadioPlayerFragment.java:349)
        at com.kaolafm.kradio.k_kaolafm.horizontal.RadioPlayerFragment$$Lambda$3.onBitmap(Unknown Source)
        at com.kaolafm.kradio.lib.utils.imageloader.GlideImageLoaderStrategy$2.onResourceReady(GlideImageLoaderStrategy.java:153)
        at com.bumptech.glide.request.SingleRequest.onResourceReady(SingleRequest.java:579)
        at com.bumptech.glide.request.SingleRequest.onResourceReady(SingleRequest.java:549)
        at com.bumptech.glide.load.engine.EngineJob.handleResultOnMainThread(EngineJob.java:218)
        at com.bumptech.glide.load.engine.EngineJob$MainThreadCallback.handleMessage(EngineJob.java:324)
        at android.os.Handler.dispatchMessage(Handler.java:98)
        at android.os.Looper.loop(Looper.java:157)
        at android.app.ActivityThread.main(ActivityThread.java:5653)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:746)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:636)

ARTHook实现

无侵入性,通用性强,兼容问题大,开源方案不能带到线上环境

七 线上内存监控方案

1、常规实现一

设定场景线上Dump:Debug.dumpHprofData(“fileName”);
如超过最大内存的80% -> 内存dump -> 回传文件(文件比较大) -> MAT手动分析

总结:Dump文件太大,和对象数正相关,可裁剪。上传失败率高,分析困难。配合一定的策略,有一些效果。

2、常规实现二

LeakCanary带到线上
预设泄漏怀疑点
发现泄漏回传

总结:不适合所有场景,必须预设怀疑点。分析比较耗时,也容易OOM。

2、LeakCanary原理

监控生命周期,onDestroy添加RefWatcher检测
二次确认断定发生内存泄漏
分析泄漏,找引用链
监控组件+分析组件

3、LeakCanary定制

预设怀疑点->自动找怀疑点(谁占用内存大)
分析泄漏链路慢->只分析Retain size大的对象
分析OOM(内存堆栈生成所有文件全部映射到内存中,占用内存)->对象裁剪,不全部加载到内存

4、线上内存监控完整方案

待机内存、重点模块内存、OOM率
整体及重点模块GC次数、GC时间
增强LeakCanary自动化内存泄漏分析

八 内存优化技巧总结

1、优化大方向

内存泄漏、内存抖动、Bitmap

2、优化细节

LargeHeap属性(提升分配内存的上限,但是更容易提升被杀的概率,然而大家都开)
onTrimMemory(低内存状态,在最严重的状态下情况图片和界面跳转到主界面,影响用户体验但是比被系统干掉要好)
使用优化过的集合:SparseArray
谨慎的使用sharepreference(第一次加载将所有数据load到内存中)
谨慎使用外部库(使用的要是经过验证的,比较成熟的库)
业务架构设计合理(如:城市数据结构,一次加载很多,可以分级为省市县等)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值