新闻类App (MVP + RxJava + Retrofit+Dagger+ARouter)性能优化之内存优化

Github地址:新闻类App (MVP + RxJava + Retrofit+Dagger+ARouter)
关于内存优化,之前写过一篇文章,android性能优化之内存优化,大家可以先看下这篇文章

内存问题

内存抖动:图形是锯齿状,GC导致卡顿
内存泄漏:可用内存减少,不断的GC
内存溢出:OOM异常,程序异常

内存优化工具

工具使用大家可以找相关资料,这里我并不介绍工具的使用
Memory Profiler
1.实时图表表示应用内存使用量,可以识别内存泄漏,内存抖动等
2.提供捕获堆转储,强制GC以及跟踪内存分配的能力

image.png

Memory Analyzer
MAT:强大的java Heap分析工具,查找内存泄漏以及内存占用
生成整体报告,分析问题等
线下使用
LeakCanary
自动检查内存泄漏工具
Github:https://github.com/square/leakcanary

内存管理机制

  • Java内存管理机制


    image.png

java内存回收算法

标记-清除算法

  • 标记出所有需要回收的对象
  • 统一回收所有被标记的对象

缺点

  • 效率不高
  • 产生大量不连续的内存碎片

复制算法

  • 将内存划分为大小相等的两块
  • 一块内存用完之后复制存活对象到另一块
  • 清理另一块内存

优缺点

  • 相对标记清除算法,实现简单,运行高效
  • 浪费一半空间,代价大

标记-整理算法

  • 标记过程与“标记-清除算法”一样
  • 存活对象往一端进行移动

优缺点

  • 避免标记-清除导致的内存碎片
  • 避免复制算法的空间浪费

分代收集算法

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

Android内存管理机制

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

Dalivk与Art区别

  • Dalivk仅固定一种回收算法
  • Art回收算法可运行期选择
  • Art具有 内存整理能力,减少内存空间

Low Memory killer

  • 进程分类
    前台进程,可见进程,服务进程,后台进程,空进程

内存抖动介绍

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

为什么内存抖动导致OOM

  • 频繁创建对象,导致内存不足及碎片
  • 不连续的内存无法被分配

内存抖动实战

首先使用Memory Profiler初步排查
使用Memory Profiler或CPU Profiler结合代码排查
模拟内存抖动代码

public class MemoryShakeActivity extends AppCompatActivity implements View.OnClickListener {

    @SuppressLint("HandlerLeak")
    private static Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            // 创造内存抖动
            for (int index = 0; index <= 100; index++){
                String arg[] = new String[100000];
            }
            mHandler.sendEmptyMessageDelayed(0,30);
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory);
        findViewById(R.id.bt_memory).setOnClickListener(this);

    }

    @Override
    public void onClick(View v) {
        mHandler.sendEmptyMessage(0);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

memory profiler出现以下情况


image.png

点击工具按钮的红色圆圈按钮,过一段时间再次点击停止记录


image.png

过一段时间下方会出现以下的情况
image.png

我们发现内存消耗最多的String[]点击String[].并选择其中的一个String[]


image.png

双击handleMessage就会跳转到相关代码

内存泄漏实战

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

代码实战

public class MemoryLeakActivity extends AppCompatActivity implements CallBack{

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memoryleak);
        ImageView imageView = findViewById(R.id.iv_memoryleak);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.avatar);
        imageView.setImageBitmap(bitmap);

        CallBackManager.addCallBack(this);
    }


    @Override
    public void dpOperate() {

    }
}
public class CallBackManager {

    public static ArrayList<CallBack> sCallBacks = new ArrayList<>();

    public static void addCallBack(CallBack callBack) {
        sCallBacks.add(callBack);
    }

    public static void removeCallBack(CallBack callBack) {
        sCallBacks.remove(callBack);
    }

}
public interface CallBack {
    void dpOperate();
}

从某个页面进去多进去几次之后会出现内存泄漏,memory profiler此时只能判断是否内存泄漏,解决还得使用MAT工具,
MAT下载地址https://www.eclipse.org/mat/

image.png

首先点击1然后点击2导出文件,但是导出的文件需要进行转换,我们需要找到自己的android sdk目录下的platform-tools目录中找到hprof-conv.exe文件,然后cmd命令找到platform-tools目录,执行命令

hprof-conv 源文件 输出文

我的是:

hprof-conv C:\Users\asus\Desktop\result.hprof C:\Users\asus\Desktop\result1.hprof

打开MAT->打开文件->Overview->Histogram->搜索MemoryLeakActivity


image.png

搜索MemoryLeakActivity之后的结果,此时有Objects有代表的确有内存泄漏8个


image.png

右击选择ListObjects->With incoming references
image.png

继续右击


image.png

结果如下


image.png

代表CallBackManager中sCallBacks持有了MemoryLeakActivity对象

解决办法MemoryLeakActivity中添加

   @Override
    protected void onDestroy() {
        super.onDestroy();
        CallBackManager.removeCallBack(this);
    }

检测不合理图片

Bitmap内存模型

  • API10之前Bitmap自身在Dalivk Heap中,像素在Native中
  • API10之后像素也放在Dalivk heap中
  • API26之后像素在Native中

获取Bitmap占用的内存

  • getByteCount
  • 一像素占用的内存

常规方式

  • 背景:图片的宽高大于控件宽高
  • 实现:继承ImageView,覆写实现计算大小

ARTHook

  • 挂钩,将额外的代码钩住原有的方法,修改执行逻辑
  • 框架:Epic(不能带到线上环境)
  • 代码
    ImageHook工具类
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.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.e(warnInfo);
    }
}

App中调用

       DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param);
            //setImageBitmap 方法名字 Bitmap 参数类型 
                DexposedBridge.findAndHookMethod(ImageView.class,"setImageBitmap",
                        Bitmap.class,new ImageHook());
            }
        });

线上内存泄漏监控

常规实现一

  • 设定场景线上Dump:Debug.dumpHprofData();
  • 实现流程
    超过最大内存的80%->内存Dump->回传文件->MAT手动分析
  • 缺点:上传失败率高,分析困难

常规实现二

  • leakCanary带到线上
  • 预设泄漏怀疑点
  • 发现泄漏回传
  • 缺点:不适合所有情况,必须预设怀疑点

LeakCanary原理(源码:后期我会单独写篇文章)

  • 监控生命周期,onDestory添加RefWatch检测
  • 二次确认断定发生内存泄漏
  • 分析泄漏,找引用链
  • 监控组件+分析组件
    image.png

    LeakCanary定制
  • 预设怀疑点->自动找怀疑点(找内存大的)
  • 分析泄漏链路慢(原因它会分析每个链路)->分析 Retain size大的对象
  • 分析OOM(leakcanary会把所有文件加载到内存)->对象裁剪,不全部加载到内存中

线上监控完整方案

  • 待机内存,重点模块内存,oom率
  • 整体及重点模块GC次数,GC时间
  • 增强的LeakCanary自动化内存泄漏分析
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容