Android性能分析与优化学习(三) App内存优化

1、内存问题
  • 内存抖动:锯齿状,GC导致卡顿
  • 内存泄漏:可用内存减少,频繁GC
  • 内存溢出:OOM、程序异常
2、工具选择
  • Memory Profiler
    实时图表展示应用内存使用量,识别内存泄露,抖动等,提供捕获堆转储、强制GC以及跟踪内存分配的能力
  • Memory Analyzer
    强大的Java Heap分析工具,查找内存泄漏及内存占用,,生成整体报告,分析问题等,线下深入使用
  • LeakCanary
    自动内存泄漏检测,只能线下使用
3、内存管理机制

Java 内存分配
方法区:类信息,常量,静态变量等,所有线程共享
虚拟机栈:局部变量表,操作数栈等,分配的引用指向堆中真正的对象
本地方法栈:native方法
堆:所有线程共享,对象分配的内存都在堆上,GC主要作用与此区域,内存泄漏也是发生在堆上
程序计数器:线程执行到了第几行
内存回收算法

  • 标记清除算法:标记出所有需要回收的对象,统一回收所有被标记的对象,效率不高,会产生大量内存碎片
    标记清除算法
  • 复制算法:将内存划分为大小相等的两块,一块内存用完后复制存活对象到另一块,清理另一块内存。实现简单,运行高效,浪费一半空间,代价大
    复制算法
  • 标记整理算法:标记过程与“标记清除算法”一样,存活对象往一端移动,清理剩余内存。避免内存碎片,避免空间浪费
    标记整理算法
  • 分代收集算法:结合多种算法优势,新生代对象存活率低,复制,老年代对象存活率高,标记整理。
    Android内存管理机制
  • 内存弹性分配,分配值与最大值受具体设备影响
  • OOM场景:内存真正不足,可用内存不足
  • Dalvak和Art区别:Dalvak仅固定一种回收算法,Art回收算法可运行期选择(比如应用在前台,响应速度最重要,应该选择简单的算法,标记清除。当应用切换到后台,可以用标记整理算法)Art具备内存整理能力,减少内存空洞
  • Low Memory Killer:当手机内存不足时候,会对所有进程回收,根据进程分类优先级和回收收益
4、内存抖动实战

内存频繁分配和回收导致内存不稳定,表现:频繁GC,内存曲线呈锯齿状,危害:导致卡顿,OOM(频繁创建对象,导致内存不足及碎片,不连续的内存碎片无法被分配,导致OOM)
写一个模拟内存抖动的activity

/**
 * 模拟内存抖动的界面
 */
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);
    }
}

执行之后发现内存分配非常频繁
内存抖动可以通过record功能记录内存分配情况image.png可以发现是string在大量分配内存,并且可以查看到代码的位置
内存抖动解决技巧:找循环或者频繁调用的地方

5、内存泄漏实战

内存中存在已经没有用的对象,表现:内存抖动,可用内存逐渐变少,危害:内存不足,GC频繁,OOM
确认问题要使用Memory Analyzer,先通过hprof-conv工具转换格式
下载地址https://www.eclipse.org/mat/downloads.php
CallBackManager 来持有activity的引用

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);
    }

}
/**
 * 模拟内存泄露的Activity
 */
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{

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

        CallBackManager.addCallBack(this);
    }

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

    @Override
    public void dpOperate() {
        // do sth
    }
}

由于callback是静态的,和应用的生命周期一样,没有释放activity的引用,造成了内存泄漏内存泄漏
使用堆转储功能,将文件保存下来image.png使用在platform-tools中的命令转换一下文件

hprof-conv .\memory-20190814T190845.hprof memory_leak_1.hprof

然后使用工具打开文件
image.pngimage.png打开Histogram,筛选出MemoryLeakActivity,选择一个Class,右键选择List objects > with incoming references,选择出哪些强引指向了我筛选
在新页面会显示通过这个class创建的对象信息,选择一个对象,右键选择Path to GC Roots > ****,通常在排查内存泄漏的时候,我们会选择exclude all phantom/weak/soft etc.references,意思是查看排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收,我们要看的就是某个对象否还存在Strong 引用链(在导出HeapDump之前要手动出发GC来保证),如果有,则说明存在内存泄漏,然后再去排查具体引用。
在这里插入图片描述
找到了内存泄漏的位置
image.png

6、ARTHook检测不合理图片
Bitmap内存模型
  • API10之前Bitmap在Dalvik Head堆中,像素在Native中,好处是不占用应用分配的内存,缺点是Java中的Bitmap被回收了但是无法控制Native内存
  • API10之后像素也在Dalvik Head堆中
  • API26后像素在Native中,加入机制,Java中的Bitmap被回收后通知Native回收
获取Bitmap占用内存
  • getByteCount 在运行时计算出
  • 宽x高x 一像素占用内存
常规方式

图片对内存优化很重要,图片宽高大于控件宽高,继承ImageView,覆写实现计算大小

ARTHook介绍

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

Epic使用

Epic 是一个在虚拟机层面、以 Java Method 为粒度的 运行时 AOP Hook 框架。简单来说,Epic 就是 ART 上的 Dexposed(支持 Android 5.0 ~ 11)。它可以拦截本进程内部几乎任意的 Java 方法调用,可用于实现 AOP 编程、运行时插桩、性能分析、安全审计等。
build.gradle 中添加如下依赖

compile 'com.github.tiann:epic:0.11.2'

继承XC_MethodHook实现逻辑,注入hook

DexposedBridge.findAndHookMethod

新建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) 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) {
                    //图片宽高大于控件宽高2倍以上
                    if (bitmap.getWidth() >= (width << 1) && bitmap.getHeight() >= (height << 1)) {
                        Log.d("ImageHookb", bitmap.getWidth() + " " + bitmap.getHeight() + " " + width + " " + height);
                    } else {
                        final Throwable stackTrace = new RuntimeException();
                        view.getViewTreeObserver().addOnPreDrawListener(() -> {
                            int w = view.getWidth();
                            int h = view.getHeight();
                            if (w > 0 && h > 0) {
                                if (bitmap.getWidth() >= (w << 1) && bitmap.getHeight() >= (h << 1)) {
                                    Log.d("ImageHookb", bitmap.getWidth() + " " + bitmap.getHeight() + " " + w+ " " + h);
                                }
                                //view.getViewTreeObserver().removeOnPreDrawListener(this);
                            }
                            return true;
                        });

                    }
                }
            }
        }
    }

}

在application的onCreate中

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());
            }
        });

在MemoryLeakActivity中

ImageView imageView = findViewById(R.id.iv_memoryleak);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
        imageView.setImageBitmap(bitmap);
		//imageViewe布局的宽高很小
      

运行之后会打印日志

7、线上内存监控方案

1、设定场景线上Dump:Debug.dumpHprofData()。 超过内存80%,dump内存,上传文件,MAT工具分析
问题:dump文件太大,和对象数相关,可剪裁。上传失败率高,分析问题困难
2、LeakCanary带到线上,预设泄漏怀疑点,发现泄漏回传
问题:不适合所有情况,分析较慢,可能会OOM
在这里插入图片描述
在这里插入图片描述

优化大方向

1、内存泄漏,2、内存抖动 ,3、bitmap

优化细节

1、LargeHeap属性 2、onTrimMemory 3、优化的集合,SparseArray 4、谨慎使用SharedPreferences 5、谨慎使用外部库 6、业务架构合理

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值