纯净、安全、绿色的下载网站

首页|软件分类|下载排行|最新软件|IT学院

当前位置:首页IT学院IT技术

Android性能优化之内存优化 浅谈Android性能优化之内存优化

handsome黄   2021-06-08 我要评论
想了解浅谈Android性能优化之内存优化的相关内容吗handsome黄在本文为您仔细讲解Android性能优化之内存优化的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Android,内存优化下面大家一起来学习吧。

1、Android内存管理机制

1.1 Java内存分配模型

先上一张JVM将内存划分区域的图

程序计数器:存储当前线程执行目标方法执行到第几行。

栈内存:Java栈中存放的是一个个栈帧每个栈帧对应一个被调用的方法。栈帧包括局部标量表
操作数栈。

本地方法栈:本地方法栈主要是为执行本地方法服务的。而Java栈是为执行Java方法服务的。
方法区:该区域被线程共享。主要存储每个类的信息(类名方法信息字段信息等)、静态变量常量以及编译器编译后的代码等。

堆:Java中的堆是被线程共享的且JVM中只有一个堆内存主要存储对象本身及数组

1.2 Dalvik和ART介绍

Dalvik:Dalvik是Google公司自己设计用于Android平台的Java虚拟机。它可以支持已转换为.dex格式的Java应用程序的运行.dex格式是专门为Dalvik应用设计的一种压缩格式适合内存和处理器速度有限的系统Dalvik经过优化允许在有限的内存中同时运行多个虚拟机实例并且每一个Dalvik应用做为独立的Linux进程执行独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。

ART:ART表示Android RuntimeDalvik是依靠一个just-In -Time编译器去解释字节码运行时编译后的应用都需要通过一个解释器在用户的设备上运行这一机制并不是特别高效但是能让应用更容易在不同的硬件和架构上运行。ART则是完全改变了这种做法在安装应用的时候就预编译字节码到机器语言这一机制叫预编译。在移除解释代码这一过程应用程序执行将更有效率启动速度更快。

ART优点:

1.系统性能更高

2.应用启动速度运行更快体验更好触感反馈更加及时。

3.更长的电池续航能力

4.支持更低的硬件

ART缺点:

1.储存空间占用更大。

2.应用安装时间更长。

Dalvik与ART区别

1.Dalvik每次都要编译在运行art只会安装时启动编译

2.art占用的空间比Dalvik要大就是用空间换时间

3.art减少编译减少CPU使用频率使用明显改善电池续航

4.art启动运行更快体验更好触感反馈更及时。

1.3 为什么要进行内存优化

1.减少oom,提高应用的稳定性
2.减少卡顿体验更好
3.减少内存占用应用存活率更高
4.提前处理掉一些异常的隐患

2、Java内存回收算法

2.1判断Java中对象是否存活的算法

2.1.1 引用计数算法

堆内存的每个对象都有一个引用计数器当对象被引用的时候计数器+1当引用失效时计数器-1当计数器的值为0时说明该对象没有被引用就会被认为是垃圾对象系统将会将其回收内存重新分配。

优点:引用计数器执行简单判定效率高。

缺点:对于循环引用的对象难以判断出来同时引用计数器增加了程序执行的开销在jdk1.1后就不在使用了。

2.1.1 根搜索法

GC Roots的对象做为起点然后向下搜索搜索所走过的路径称为引用链当一个对象到GC Roots没有任何引用链相连时则该对象不可达也就是说该对象为为垃圾对象可以被回收。
在Java中可以做为GC Roots的对象包括一下四种:

1.虚拟机栈中引用的对象

2.方法区中的类静态属性引用的对象

3.方法区中常量引用的对象

4.本地方法栈中JNI的引用对象

2.2 JVM垃圾回收算法

2.2.1 标记清除法

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

缺点:效率低其次会产生大量的不连续的内存碎片导致提前触发另一次垃圾收集动作。

2.2.2 复制回收算法

复制回收算法是将可用内存按容量分成大小相等的两块每次只使用其中的一块当这块内存使用完了就将存活的对象复制到另一块内存上去然后把使用过的内存空间一次清理掉这样使得每都次都是对其中一块内存进行回收内存分配时不用考虑内存碎片等复杂情况。

缺点:可使用内存降为原来的一半。

2.2.3 标记整理法

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

优点:相比于标记清除法来说标记整理法不会大量产生不连续内存碎片问题。

缺点:如果是在对象存活率较高的情况下会执行较多的复制操作效率将会降低很多而在存活率较低的情况下效率会大大提高。

2.2.4 分代收集回收算法

当前商业虚拟机都是采用的是分代收集算法根据对象存活的周期不同将内存划分为几块一般是将java堆分为年轻代老年代和永久代。然后根据各个年代的特点来采取不同收集算法年轻代存活率较低采用复制回收算法老年代对象存活率较高采用标记清除法或者是标记整理法来进行回收。

3、内存问题表现形式

3.1 内存抖动

内存波动图呈锯齿状gc频繁导致卡顿。

3.2 内存泄漏

内存泄露简单来说就是系统分配出去的内存由于某种原因导致没法释放内存会越来越小最终导致oom。

3.3 内存溢出

即OOMOOM时会导致程序异常。Android设备出厂以后java虚拟机对单个应用的最大内存分配就确定下来了超出这个值就会OOM。

4、内存优化常用工具

4.1 Memory Profiler

Memory Profiler是Android studio自带的工具实时图表形式展示应用内存使用的情况可以用来识别内存泄露抖动等
注意:如果在控制台中没有找到Profiler可View -----> Tool Windows ---> Profiler 进行打开

优点:方便直观便于线下使用

4.2 Memory Analyzer(MAT)

1、强大的java heap分析工具查找内存泄露及内存占用

2、生成整体报告便于分析问题

3、可以在线下深入使用

MAT使用:

MAT下载地址:https://www.eclipse.org/mat/downloads.php

获取hprof文件

导出来的Dump是没法直接使用mat打开的Android SDK自带了一个转换工具在SDK的platform-tools下其中转换语句为:

cd D:\aa\sdk\platform-tools

hprof-conv aaa.hprof  bbb.hprof

注:aaa.hprof表示从profiler中导出来的dump文件bbb.hprof 表示转化出来的dump文件

使用mat打开转化出来的dump

MAT视图

在MAT窗口上OverView是一个总体概览显示总体的内存消耗情况和疑似问题。

1、Histogram:列出内存中的所有实例对象和个数以及大小在顶部regex区域支撑正则表达式查找

2、Dominator Tree:列出最大的对象及其依赖存活的Object相比于Histogram能更方便的看出引用关系。

3、Top Consumers:通过图像列出最大的Object

4、Leak Suspects:通过MAT自动分析内存泄露的原因和泄露的一份总体报告

其中分析内存情况我们基本用到的就是Histogram和Dominator Tree

Class Name:类名。

Objects:对象实例个数。

Shallow Heap:对象自身占用内存大小不包括它引用的对象

Retained Heap:是当前对象大小和直接或者间接引用到的对象大小总和包括递归释放的。

查找内存泄露方式

步骤一:在Regex通过包名进行匹配当然也可以通过其他方式进行匹配

步骤二:右键选中怀疑对象List objects --> with incoming references

注 with outgoing references 他引用了那些对象

with incoming references 那些对象引用了他

步骤三:选择当前的一个 Path to GC Roots/Merge to GC Roots 的 exclude All 弱软虚引用。

图标的左下角出现这个则表示出现了内存泄露。然后回调代码中分析即可。

4.3 LeakCanary

使用

implementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'

application中

public class App extends Application {

    private RefWatcher mRefWatcher;

    @Override
    public void onCreate() {
        super.onCreate();
     mRefWatcher = LeakCanary.install(this);
    }

    public static RefWatcher getRefWatcher(Context context) {
        App application = (App) context.getApplicationContext();
        return application.mRefWatcher;
    }
 }

在activity或者fragment中的onDestory()方法调用

RefWatcher refWatcher = App.getRefWatcher(getActivity());
refWatcher.watch(this);

原理

主要是通过WeakReference + ReferenceQueue来判断对象是否被系统GC回收WeakReference创建时传入一个ReferenceQueue对象当WeakReference引用的对象生命周期结束后会被添加到ReferenceQueue中当GC过后对象一直没有被添加进入到ReferenceQueue可能就会存在内存泄露再次触发GC二次确认。

5、常见的内存泄露

1、资源性对象未关闭

对于资源性对象不再使用时应该立即调用它的close()函数将其关闭然后再置为null。例如Bitmap等资源未关闭会造成内存泄漏此时我们应该在Activity销毁时及时关闭。

2、注册对象未注销

例如BraodcastReceiver、EventBus未注销造成的内存泄漏我们应该在Activity销毁时及时注销。

3、类的静态变量持有大数据对象

尽量避免使用静态变量存储数据特别是大数据对象建议使用数据库存储。

4、单例造成的内存泄漏

优先使用Application的Context如需使用Activity的Context可以在传入Context时使用弱引用进行封装然后在使用到的地方从弱引用中获取Context如果获取不到则直接return即可。

5、非静态内部类的静态实例

该实例的生命周期和应用一样长这就导致该静态实例一直持有该Activity的引用Activity的内存资源不能正常回收。此时我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例如果需要使用Context尽量使用Application Context如果需要使用Activity Context就记得用完后置空让GC可以回收否则还是会内存泄漏。

6、Handler临时性内存泄漏

Message发出之后存储在MessageQueue中在Message中存在一个target它是Handler的一个引用Message在Queue中存在的时间过长就会导致Handler无法被回收。如果Handler是非静态的则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息当这个Activity退出时消息队列中还有未处理的消息或者正在处理的消息并且消息队列中的Message持有Handler实例的引用Handler又持有Activity的引用所以导致该Activity的内存资源无法及时回收引发内存泄漏。解决方案如下所示:

  • 1、使用一个静态Handler内部类然后对Handler持有的对象(一般是Activity)使用弱引用这样在回收时也可以回收Handler持有的对象。
  • 2、在Activity的Destroy或者Stop时应该移除消息队列中的消息避免Looper线程的消息队列中有待处理的消息需要处理。需要注意的是AsyncTask内部也是Handler机制同样存在内存泄漏风险但其一般是临时性的。对于类似AsyncTask或是线程造成的内存泄漏我们也可以将AsyncTask和Runnable类独立出来或者使用静态内部类。

7、容器中的对象没清理造成的内存泄漏

在退出程序之前将集合里的东西clear然后置为null再退出程序

8、WebView

WebView都存在内存泄漏的问题在应用中只要使用一次WebView内存就不会被释放掉。我们可以为WebView开启一个独立的进程使用AIDL与应用的主进程进行通信WebView所在的进程可以根据业务的需要选择合适的时机进行销毁达到正常释放内存的目的。

9、使用ListView时造成的内存泄漏

在构造Adapter时使用缓存的convertView。

6、优化内存空间的方式

6.1、java对象的引用

强引用:我们平时开发写的代码基本百分之九十九的都是强引用。

软引用:如果一个对象具有软引用那么当内存不足时就会回收它。

弱引用:GC时只要发现有弱引用那么就会回收它当然有可能存在GC多次才发现

虚引用:虚引用必须要和引用队列关联起来使用。任何时候都有可能被垃圾回收器回收。一般可以用来判断GC的频率GC频率过高那么说明内存出了问题。同时也可以监听某个重要的对象是否被回收。

所以在平时我们编写代码的时候适当的使用软引用弱引用对我们的内存优化也能起到重要的作用。

6.2、减少不必要的内存开销

1、AutoBoxing

自动装箱的核心是吧基础数据类型转换成对应的包装类比如int 类型只是占用4字节但是Integer对象占用16字节。

2、内存复用

资源复用:通用的字符串颜色定义简单页面布局的复用

视图复用:进行布局复用

3、使用优化过的数据类型

如 SparseArray、SparseBooleanArray、LongSparseArray使用这些API可以让我们的程序更加高效。HashMap 工具类会相对比较 低效因为它 需要为每一个键值对都提供一个对象入口而 SparseArray 就 避免 掉了 基本数据类型转换成对象数据类型的时间。

4、项目中少用枚举

枚举占用内存是常量三倍。

5、在应用可以内存过低时主动释放内存

在application中的 onTrimMemory/onLowMemory内存紧张时会回调该方法我们可以在这个方法中释放掉图片缓存静态缓存来避免被kill。

6、避免创建一些不必要的对象

如在字符串拼接时不要用“+”来进行拼接而是使用StringBufferStringBuilder来替代。因为String 内部是被final修饰的不可继承使用+进行拼接是会产生一个新的对象而占用内存。

7、尽量不要在一些循环的地方创建对象。

如自定义的时候在onDraw()方法。

7、优雅的检测大图

项目中会经常遇到这样的情况我们的布局中控件的宽高可能只是50 * 50 但是从服务器给过来的图片或者是UI给过来的图片往往会大很多而如果图片在资源文件下还好可以直接查看宽高但是如果从服务器上获取到的呢这是我们经常会忽略的。而图片过大占用的内存就更多这是没有必要的。那么我们怎么检测出服务器给过来的图片过大的呢?

7.1、继承ImageView 重新实现onDraw()

这种方法我们可以重新测量图片的宽高超过一定的范围我们就可以输出警告。但是这种方法对代码侵入性很强。如果是有新同学加入容易造成代码混乱。

7.2、ARTHook

Hook的意思是钩子也就是在消息过去之前可以把消息勾住不让其传递能够针对不同的消息或者api在执行之前先执行我们自己的操作。

这里推荐使用Epic 框架:https://github.com/tiann/epic

添加依赖

implementation 'me.weishu:epic:0.3.6'

创建一个ImageHook类

package com.optimize.performance.memory;

import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;

import com.optimize.performance.utils.LogUtils;
import com.taobao.android.dexposed.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);
    }

}

在application中

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

这样在开发者调用setImageBitmap 来设置图片的时候都会进行对图片的宽高进行比如如果超出一定的范围则进行提示。


相关文章

猜您喜欢

  • Springboot全局时间格式化 Springboot 全局时间格式化操作

    想了解Springboot 全局时间格式化操作的相关内容吗陈晨辰~在本文为您仔细讲解Springboot全局时间格式化的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Springboot,全局时间格式化下面大家一起来学习吧。..
  • Vue插槽slot用法 Vue中插槽slot如何使用与应用场景详析

    想了解Vue中插槽slot怎样使用与应用场景详析的相关内容吗JDragon在本文为您仔细讲解Vue插槽slot用法的相关知识和一些Code实例欢迎阅读和指正我们先划重点:vue中slot的用法,vue,插槽slot,vue作用域插槽下面大家一起来学习吧。..

网友评论

Copyright 2020 www.Videogametimes.com 【视游时光】 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 点此查看联系方式