精华内容
下载资源
问答
  • App性能优化内存优化

    千次阅读 2020-01-03 14:22:41
    App性能优化内存优化 本篇主要探讨app性能优化相关的知识点,预计20年2月底前完成 内存优化工具 内存管理机制 内存都懂解决 内存泄漏解决 MAT详解 小结 ...

    App性能优化:内存优化

    目录:

    1. 内存优化工具
    2. 内存管理机制
    3. 内存抖动解决
    4. 内存泄漏解决
    5. MAT详解
    6. 小结
    背景介绍
    • 内存是大问题但缺乏关注
    • 压死骆驼的最后一根稻草
    表现形式
    • 内存抖动:锯齿状,GC导致卡顿
    • 内存泄漏:可用内存减少、频繁GC
    • 内存溢出:OOM、程序异常

    1、工具选择

    • Memory Profiler
    • Memory Analyzer
    • LeakCanary
    Memory Profiler
    1. 实时图标展示内存使用量
    2. 识别内存泄漏、抖动
    3. 提供捕获堆转储、强制GC以及跟踪内存分配的能力
    Memory Profiler的使用
    1. 非常直观
    2. 线下平时使用
    Memory Analyzer(MAT)
    1. 强大的Java heap 分析工具,帮助查找内存泄漏和内存占用
    2. 生成整体报告,分析问题
    3. 线下深入使用
    LeakCanary
    1. 自动内存泄漏检测
    2. 线下集成

    2、android内存管理机制

    Java内存管理机制

    在这里插入图片描述

    Java内存回收算法-标记清除算法
    • 标记出所有需要回收的对象
    • 统一回收所有被标记的对象
      在这里插入图片描述
    标记回收算法存在的问题
    • 标记和清除的效率不高
    • 产生大量的内存碎片
    Java内存回收算法之 复制算法
    • 将内存分成两份,只将数据存储在其中一块上。
    • 当需要垃圾回收时,也是标记出废弃的数据,然后将有用数据复制到另一块内存上,最后将第一块内存全部清除
      在这里插入图片描述
    复制算法的问题
    • 实现简单,运行高效
    • 浪费一半空间,代价大
    Java内存回收算法之 标记整理算法
    1. 标记过程与“标记-清除算法”一样
    2. 存活对象往一端移动
    3. 清理其余内存
      在这里插入图片描述
    标记整理算法的特点
    1. 避免标记-清理导致的内存碎片
    2. 避免复制算法的空间浪费
    Java内存回收算法之 分代收集算法
    1. 结合多种收集算法优势
    2. 新生带对象存活率低:复制算法
    3. 老年代对象存活率高:标记-整理算法
    Android内存管理机制
    • 内存弹性分配,分配值和最大值受具体设备影响
    • OOM场景:内存真正不足,可用内存不足
    Dalvik 与Art区别
    • Dalvik仅固定一种回收算法
    • Art回收算法可运行期选择
    • Art具备内存整理能力,减少内存空洞
    Low Memory killer
    • 进程分类:前端、可见、服务、后台、空进程,优先级依次降低
    • 回收收益

    内存抖动案例

    ######制造内存抖动

        companion object {
            var datas: ArrayList<TestClass>? = null
            val handler = object : Handler() {
                override fun handleMessage(msg: Message) {
                    //创造内存抖动
                    for (i in 0..100) {
                        datas = ArrayList<TestClass>(100000)
                    }
                    sendEmptyMessageDelayed(0, 1000)
                }
            }
        }
    

    在这里插入图片描述

    内存抖动解决技巧
    1. 找循环或者频繁调用的地方
    2. 通过Profiler定位,锯齿状或者频繁的GC

    内存泄漏

    • 定义:内存中存在没有用的对象,又无法回收
    • 表现:内存抖动、可用内存逐渐减少

    内存泄漏案例

    1. 静态变量持有Activity引用导致Activity无法被回收
    //定义一个接口
    public interface Callback {
    
        void doOperation();
    }
    //定义持有Activity引用的类
    public class CallbackMananger {
        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);
        }
    }
    
    
    1. 在生成LeakCanaryActivity,显示一张图片
    
    class MemoryLeakActivity : AppCompatActivity(), Callback {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_memory_leak)
            val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_leaktest)
            iv.setImageBitmap(bitmap)
            //保存Activity引用
            CallbackMananger.addCallback(this)
        }
    
        override fun doOperation() {
    
        }
    }
    
    
    1. 重复打开关闭 页面,即可在Profiler中看到内存成阶梯状上升,如下图所示:
      在这里插入图片描述
      显然:这里每次GC的的时候并没有导致MemoryLeakActivity被回收,内存图形呈现阶梯状显示。

    2. 使用MAT分析内存泄漏
      1. 进入官网下载MAT工具:MAT下载地址
      2. 下载对应系统版本
      在这里插入图片描述

    3. 安装MAT工具,双击zip文件解压后得到mat.app
      在这里插入图片描述

    4. 获取堆文件 Heap Dump
      在这里插入图片描述

    5. 将文件存到本地
      在这里插入图片描述
      在这里插入图片描述

    6. 注意这里是Android的hprof文件,需要使用android 的platform-tool里面的hprof-conv工具对文件进行一次格式转化,转化命令如下:
      ./hprof-conv /Users/hudingpeng/Desktop/memory-20200221T114049.hprof 2.hprof
      在这里插入图片描述

    7. 这样在platform-tools目录下会生成一个2.hprof文件,如下图所示:
      在这里插入图片描述

    8. 打开MAT工具:

      1. 直接点击mat.app会报错,按照以下步骤运行
      2. 右键mat.app => 显示包内容 =>进入 mat.app/Contents/MacOS ,此目录下就有工具MemoryAnalyzer =>双击直接打开工具就行了
        在这里插入图片描述
    9. 在这里插入图片描述
      10.点击open a Heap Dump选择我们的转化后的文件2.hprof 文件即可,下面分析MAT工具的一些常用用法

    MAT工具分析内存泄漏

    1. 打开hprot文件,选择Histogram
      在这里插入图片描述

    2. 搜索我们的泄漏的MemoryLeakActivity,可以看到有4个MemoryLeakActivity对象,表示已经泄漏了
      在这里插入图片描述

    3. 右键选择查看with incoming references
      在这里插入图片描述

    4. 选择一个,点击查看到GC Root的路径,看到具体引用的地方
      在这里插入图片描述
      在这里插入图片描述
      这样我们就看到了具体泄漏的地方就是,CallbackManager类里面的静态变量sCallbacks

    5. 点击Group by package 可以按包名排序
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      这样也可以很方便看到我们的包名下的类对象生成的情况,这里也可以看到MemoryLeakActivity在内存里有4个对象

    6. 第三个菜单项:dominator_tree 视图,显示对象占用内存的百分比
      在这里插入图片描述
      可以看到对象占用内存的百分比,给我们的内存优化提供一定的参考

    7. 第四个菜单项:OQL,相当于一种数据查询语言,我们直接搜索我们的类,点击感叹号查询,如下图所示
      在这里插入图片描述

    8. Top Consumer条目:通过图表的形式显示占用内存最多的对象
      在这里插入图片描述
      在这里插入图片描述
      这对我们的内存占用优化也是一个不错的参考

    关于BitMap的优化

    Btimap的内存模型
    1. 获取Bitmap占用的内存

      1. getByteCount()方法
      2. 宽 x 高 x 一像素占用的内存
    2. 常规方式:
      背景:图片对内存大小至关重要、图片宽高大于控件宽高

    3. 通过Epic ARTHook优雅检测不合理的图片
      Epic:Epic 是一个在虚拟机层面、以 Java Method 为粒度的 运行时 AOP Hook 框架。简单来说,Epic 就是 ART 上的 Dexposed(支持 Android 4.0 ~ 10.0)。它可以拦截本进程内部几乎任意的 Java 方法调用,可用于实现 AOP 编程、运行时插桩、性能分析、安全审计等。
      Epic 被 VirtualXposed 以及 太极 使用,用来实现非 Root 场景下的 Xposed 功能,已经经过了相当广泛的验证。
      非常厉害的AOP框架,参考连接:Epic AOP框架
      这里我们Hook setImageBitmap方法:

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

    在App onCreate方法里注册要Hook的方法,传入ImageHook

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

    more:LeakCanary 监控内存泄漏

    内存优化总结

    优化大方向
    1. 内存泄漏
    2. 内存抖动
    3. Btimap
    优化细节
    1. LargeHeap属性 (建议开启,大概率能申请到更多内存)
    2. onTrimMemory (系统块干掉app了,可以手动清理界面,跳转到主界面)
    3. 使用系统优化过的集合:SparseArray
    4. 谨慎使用SharedPreferences
    5. 谨慎使用外部库
    6. 业务架构设计合理
    展开全文
  • 上篇博客我们写到了 Android 中内存泄漏的检测以及相关案例,这篇我们继续来分析一下 Android 内存优化的相关内容。  上篇:[Android 性能优化内存泄漏检测以及内存优化(上)]...

    上篇博客我们写到了 Android 中内存泄漏的检测以及相关案例,这篇我们继续来分析一下 Android 内存优化的相关内容。
      上篇:Android 性能优化之内存泄漏检测以及内存优化(上)
      中篇:Android 性能优化之内存泄漏检测以及内存优化(中)
      下篇:Android 性能优化之内存泄漏检测以及内存优化(下)
      转载请注明出处:http://blog.csdn.net/self_study/article/details/68946441
      对技术感兴趣的同鞋加群544645972一起交流。

    Android 内存优化

    上篇博客描述了如何检测和处理内存泄漏,这种问题从某种意义上讲是由于代码的错误导致的,但是也有一些是代码没有错误,但是我们可以通过很多方式去降低内存的占用,使得应用的整体内存处于一个健康的水平,下面总结一下内存优化的几个点:

    图片处理优化

    由于图片在应用中使用的较为频繁,而且图片占用的内存通常来说也比较大,举个例子来说,现在正常的手机基本都在 1000W 像素左右的水平,较好的基本都在 1600W 像素,这时候拍出来的照片基本都在 3400*4600 这个水平,按照 ARGB_8888 的标准,一个像素 4 个字节,所以总共有 1600W*4=6400W 字节,总共 64M,也就是说会占用 64M 的内存,而实际出来的 .png 图片大小也就才 3M 左右,这是一个非常恐怖的数量,因为对于一个 2G 左右内存的手机来说,一个进程最大可用的内存可能也就在 200M+,一张图片就能够占用一半内存,这也就是为什么 decode 一个 bitmap 是发生 OOM 高频的地方,所以在实际开发过程中图片的处理和内存占用优化也是一个比较重要的地方。
      Android中图片有四种属性,分别是:

    • ALPHA_8:每个像素占用1byte内存
    • ARGB_4444:每个像素占用2byte内存
    • ARGB_8888:每个像素占用4byte内存 (默认)
    • RGB_565:每个像素占用2byte内存

    大图片优化

    为了找出在运行过程中占用内存很大的图片,这个时候就可以借助上篇博客介绍到的 MAT 了,按照 Retained Heap 大小进行排序,找出占用内存比较大的几个对象,然后通过引用链找到持有它的地方,最后看能否有优化的地方。

    图片分辨率相关

    我们一般将不同分辨率的图片放置在不同的文件夹 hdpi/xhdpi/xxhdpi 下面进行适配,通过 android:background 来设置背景图片或者使用 BitmapFactory.decodeResource() 方法的时候,图片默认情况下会进行缩放,在 Java 层实际调用的是 BitmapFactory 里的 decodeResourceStream 方法:

    /**
     * Decode a new Bitmap from an InputStream. This InputStream was obtained from
     * resources, which we pass to be able to scale the bitmap accordingly.
     */
    public static Bitmap decodeResourceStream(Resources res, TypedValue value,
            InputStream is, Rect pad, Options opts) {
    
        if (opts == null) {
            opts = new Options();
        }
    
        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }
        
        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        
        return decodeStream(is, pad, opts);
    }
    

    decodeResourceStream 在解析时会将 Bitmap 根据当前设备屏幕像素密度 densityDpi 的值进行缩放适配操作,使得解析出来的 Bitmap 与当前设备的分辨率匹配,达到一个最佳的显示效果,上面也提到过,解析过后 Bitmap 的大小将比原始的大不少,关于 Bitmap 的详细分析可以看一下这篇博客:Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?
      关于 Density、分辨率和相关 res 目录的关系如下:

    DensityDpi分辨率resDensity
    160dpi320 x 533mdpi1
    240dpi460 x 800hdpi1.5
    320dpi720 x 1280xhdpi2
    480dpi1080 x 1920xxhdpi3
    560dpi1440 x 2560xxxhdpi3.5

    举个例子来说一张 1920x1080 的图片来说,如果放在 xhdpi 下面,那么 xhdpi 设备将其转成 bitmap 之后的大小是 1920x1080,而 xxhdpi 设备获取的大小则是 2520x1418,大小约为前者的 1.7 倍,这些内存对于移动设备来说已经算是比较大的差距。有一点需要提到的是新版本 Android Studio 已经使用 mipmap 来代替了,比起 drawable 官方的解释是系统会在缩放上提供一定的性能优化:

    Mipmapping for drawables
    
    Using a mipmap as the source for your bitmap or drawable is a simple way to provide a quality image and various image scales, which can be particularly useful if you expect your image to be scaled during an animation.
    
    Android 4.2 (API level 17) added support for mipmaps in the Bitmap class—Android swaps the mip images in your Bitmap when you've supplied a mipmap source and have enabled setHasMipMap(). Now in Android 4.3, you can enable mipmaps for a BitmapDrawable object as well, by providing a mipmap asset and setting the android:mipMap attribute in a bitmap resource file or by calling hasMipMap().
    

    但是从用法来说和正常的 drawable 一样。
      系统也对图片展示进行了相应的优化,对于类似在 xml 里面直接通过 android:background 或者 android:src 设置的背景图片,以 ImageView 为例,最终会调用 ResourceImpl(低版本是 Resource) 类中的里的 loadDrawable 方法,在这个方法中我们可以很清楚的看到系统针对相同的图片使用享元模式构造了一个全局的缓存 DrawableCache 类的对象:

    Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
            boolean useCache) throws NotFoundException {
        try {
            if (TRACE_FOR_PRELOAD) {
                // Log only framework resources
                if ((id >>> 24) == 0x1) {
                    final String name = getResourceName(id);
                    if (name != null) {
                        Log.d("PreloadDrawable", name);
                    }
                }
            }
    
            final boolean isColorDrawable;
            final DrawableCache caches;
            final long key;
            if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
                    && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
                isColorDrawable = true;
                caches = mColorDrawableCache;
                key = value.data;
            } else {
                isColorDrawable = false;
                caches = mDrawableCache;
                key = (((long) value.assetCookie) << 32) | value.data;
            }
    
            // First, check whether we have a cached version of this drawable
            // that was inflated against the specified theme. Skip the cache if
            // we're currently preloading or we're not using the cache.
            if (!mPreloading && useCache) {
                final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                if (cachedDrawable != null) {
                    return cachedDrawable;
                }
            }
            .....
    }
    

    DrawableCache 类继承自 ThemedResourceCache 类,来看看这两个相关类:

    /**
     * Class which can be used to cache Drawable resources against a theme.
     */
    class DrawableCache extends ThemedResourceCache<Drawable.ConstantState> {
        ......
    }
    
    /**
     * Data structure used for caching data against themes.
     *
     * @param <T> type of data to cache
     */
    abstract class ThemedResourceCache<T> {
        private ArrayMap<ThemeKey, LongSparseArray<WeakReference<T>>> mThemedEntries;
        private LongSparseArray<WeakReference<T>> mUnthemedEntries;
        private LongSparseArray<WeakReference<T>> mNullThemedEntries;
        .....
    }
    

    可以看到这个类使用一个 ArrayMap 来存储一个 Drawable 和这个 Drawable 对应的 Drawable.ConstantState 信息,相同的图片对应相同的 Drawable.ConstantState,所以这就可以保证在一些情况下相同的图片系统只需要保存一份,从而减少内存占用。我们从这里可以得到一些启示,如果我们在某些会重复使用图片的场景下,自己构造一个 Bitmap 缓存器,然后里面保存 Bitmap 的 WeakReference,当使用的时候先去缓存里面获取,获取不到再做解析的操作。

    图片压缩

    BitmapFactory 在 decode 图片的时候,可以带上一个 Options,这个很多人应该很熟悉,在 Options 中我们可以指定使用一些压缩的功能:

    • inTargetDensity
    • 表示要被画出来时的目标像素密度;
    • inSampleSize
    • 这个值是一个 int,当它小于 1 的时候,将会被当做 1 处理,如果大于 1,那么就会按照比例(1 / inSampleSize)缩小 bitmap 的宽和高、降低分辨率,大于 1 时这个值将会被处置为 2 的指数(3 会被处理为 4,5被处理为8)。例如 width=100,height=100,inSampleSize=2,那么就会将 bitmap 处理为,width=50,height=50,宽高降为 1/2,像素数降为 1/4;
    • inJustDecodeBounds
    • 字面意思就可以理解就是只解析图片的边界,有时如果只是为了获取图片的大小就可以用这个,而不必直接加载整张图片;
    • inPreferredConfig
    • 默认会使用 ARGB_8888,在这个模式下一个像素点将会占用 4 个字节,而对一些没有透明度要求或者图片质量要求不高的图片,可以使用 RGB_565,这样一个像素只会占用 2 个字节,一下就可以省下 50% 内存了;
    • inPurgeable 和 inInputShareable
    • 这两个需要一起使用,BitmapFactory 类的源码里面有注释,大致意思是表示在系统内存不足时是否可以回收这个 Bitmap,有点类似软引用,但是实际在 5.0 以后这两个属性已经被忽略,因为系统认为回收后再解码实际反而可能会导致性能问题;
    • inBitmap
    • 官方推荐使用的参数,表示重复利用图片内存,减少内存分配,在 4.4 以前只有相同大小的图片内存区域可以复用,4.4 以后只要原有的图片比将要解码的图片大就可以实现复用了。
      关于图片压缩和图片内存优化的例子可以参考我以前写的一个博客: android仿最新版本微信相册–附源码

    巨型图片的处理

    要加载一张巨型的图片,比如 20000*10000 分辨率的,这个时候全放进内存是完全不可能的,直接会占用 800M 内存,所以必须要用到上面说到的压缩比,将其分辨率降低到和屏幕匹配,匹配之后如果还要去支持用户的放大、缩小、左右滑动等操作,这时候就可以使用 BitmapRegionDecoder 这个类去处理图片了,具体的可以去看看这篇博客:Android 高清加载巨图方案 拒绝压缩图片,实现的原理就是分区域去加载,或者可以去参考这个开源库:WorldMap

    图片缓冲池

    现在默认的图片加载工具例如 Universal-ImageLoader 或者 Glide 都会使用一个 LruCache 来管理应用中的图片缓存,一般缓冲池的大小设置为应用可用内存的 1/8。

    有效利用系统自带资源

    Android 系统本身内置了大量的资源,比如一些通用的字符串、颜色定义、常用 icon 图片,还有些动画和页面样式以及简单布局,如果没有特别的要求,这些资源都可以在应用程序中直接引用。直接使用系统资源不仅可以在一定程度上减少内存的开销,还可以减少应用程序 APK 的体积。

    内存抖动造成内存碎片优化

    上篇博客说到过频繁的 GC 会造成内存的抖动,最终会导致内存当中存在很多内存碎片,虽然总体来说内存是可用的,但是当分配内存给一个大对象的时候,没有一块足够大的连续区域可以分配给这个对象就会造成 OOM,所以这个时候为了减少内存抖动,需要去观察 Memory Monitor,检查应用的正常使用过程中有没有因为频繁的内存分配和释放导致锯齿形状的内存图,如果有的话去检查相关代码,比较容易出现内存抖动的地方可能是 convertView 没有复用、频繁拼接小的 String 字符串、在 for 循环中创建对象等等,找到问题所在,解决内存抖动。

    常用数据结构优化

    ArrayMap 以及 SparseArray 是 Android 系统专门为移动设备而定制的数据结构,用于在一定情况下取代 HashMap 而达到节省内存的目的,对于 key 为 int 的 HashMap 尽量使用 SparceArray 替代(一般 Lint 也会提示开发者将其换成 SparceArray),大概可以省30%的内存,而对于其他类型,ArrayMap 对内存的节省实际并不明显,10% 左右,但是数据量在 1000 以上时,查找速度可能会变慢,具体的可以看看这篇博客:HashMap,ArrayMap,SparseArray源码分析及性能对比

    避免创建不必要的对象

    最常见的例子就是当你要频繁操作一个字符串时,使用 StringBuilder 代替 String。对于所有基本类型的组合:int 数组比 Integer 数组好,这也概括了一个基本事实,两个平行的 int 数组比 (int,int) 对象数组性能要好很多。总体来说,就是避免创建短命的临时对象。减少对象的创建就能减少垃圾收集,进而减少对用户体验的影响。

    尽量避免使用枚举

    Android 平台上枚举是比较争议的,在较早的 Android 版本,使用枚举会导致包过大,在某些情况下使用枚举甚至比直接使用 int 包的 size 大了 10 多倍。在 Stackoverflow 上也有很多的讨论,大致意思是随着虚拟机的优化,目前枚举变量在 Android 平台性能问题已经不大,而目前 Android 官方建议,使用枚举变量还是需要谨慎,因为枚举变量可能比直接用 int 多使用 2 倍的内存,具体的可以看看这个讨论:Should I strictly avoid using enums on Android?

    减少 View 的层级

    虽然这或多或少有点渲染优化的味道,但是由于 View 也是会占用一定内存的,所以第一步是通过 Hierarchy Viewer 去去掉多余的 View 层级,第二步是通过使用 ViewStub 去对一些可以延迟加载的 View 做到使用时加载,一定程度上也可以降低内存使用。

    数据相关

    使用 Protocol Buffer 对数据进行压缩(关于 Protocol Buffer 和其他工具的对比,可以看看这篇文章:thrift-protobuf-compare),Protocol Buffer 相比于 xml 可以减少 30% 的内存使用量;慎用 SharedPreference,因为对于同一个 SP 有时候为了读取一个字段可能会将整个 xml 文件都加入内存,因此慎用 SP,或者可以将一个大的 SP 分散为几个小的 SP;数据库字段尽量精简,表设计合理,只读取所需要的字段而不是整个结构都加载到内存当中。

    dex 优化,代码优化,谨慎使用外部库

    有人觉得代码多少与内存没有关系,实际上会有那么点关系,现在稍微大一点的项目动辄就是百万行代码以上,多 dex 也是常态,不仅占用 Rom 空间,实际上运行时候需要加载的 dex 也是会占用内存的(几 M),有时候为了使用一些库里的某个功能函数就引入了整个庞大的库是不太合适的,此时可以考虑抽取必要部分;另外开启 proguard 优化代码,使用 Facebook redex 优化 dex(好像有不少坑)也是一种不错的方式。

    对象池模式享元模式

    对于对象的重复使用来说,对象池模式享元模式再合适不过了,具体的可以去看看我博客里面对于这两个模式的介绍和使用。

    onLowMemory() 与 onTrimMemory()

    我们都知道 Android 用户可以随意在不同的应用之间进行快速切换,系统为了让 Background 的应用能够迅速的切换到 Forground,每一个 Background 的应用都会占用一定的内存。Android 系统会根据当前的系统的内存使用情况,在一定情况下决定回收部分 Background 的应用内存,如果 Background 的应用从暂停状态直接被恢复到 Forground,能够获得较快的恢复体验,如果 Background 应用是从 Kill 状态进行恢复,相比之下就显得稍微有点慢:
    这里写图片描述

    • onLowMemory()
    • Android 系统提供了一些回调来通知当前应用的内存使用情况,通常来说当所有的 Background 应用都被 kill 掉的时候,Forground 应用会收到 onLowMemory() 的回调,在这种情况下需要尽快释放当前应用的非必须的内存资源,从而确保系统能够继续稳定运行。
    • onTrimMemory(int)
    • Android 系统从 4.0 开始还提供了 onTrimMemory() 的回调,当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调,同时在这个回调里面会传递指定的参数,代表不同的内存使用情况,收到 onTrimMemory() 回调的时候,需要根据传递的参数类型进行判断,合理的选择释放自身的一些内存占用,一方面可以提高系统的整体运行流畅度,另外也可以避免自己被系统判断为优先需要杀掉的应用,返回的参数:
      • [TRIM_MEMORY_BACKGROUND](https://developer.android.com/reference/android/content/ComponentCallbacks2.html#TRIM_MEMORY_BACKGROUND)
      • [TRIM_MEMORY_COMPLETE](https://developer.android.com/reference/android/content/ComponentCallbacks2.html#TRIM_MEMORY_COMPLETE)
      • [TRIM_MEMORY_MODERATE](https://developer.android.com/reference/android/content/ComponentCallbacks2.html#TRIM_MEMORY_MODERATE)
      • [TRIM_MEMORY_RUNNING_CRITICAL](https://developer.android.com/reference/android/content/ComponentCallbacks2.html#TRIM_MEMORY_RUNNING_CRITICAL)
      • [TRIM_MEMORY_RUNNING_LOW](https://developer.android.com/reference/android/content/ComponentCallbacks2.html#TRIM_MEMORY_RUNNING_LOW)
      • [TRIM_MEMORY_RUNNING_MODERATE](https://developer.android.com/reference/android/content/ComponentCallbacks2.html#TRIM_MEMORY_MODERATE)
      • [TRIM_MEMORY_UI_HIDDEN](https://developer.android.com/reference/android/content/ComponentCallbacks2.html#TRIM_MEMORY_UI_HIDDEN)
    因为 onTrimMemory() 的回调是在 API 14 才被加进来的,对于老的版本,你可以使用 onLowMemory 回调来进行兼容,onLowMemory 相当与 TRIM_MEMORY_COMPLETE。

    谨慎使用多进程

    使用多进程可以把应用中的部分组件运行在单独的进程当中,这样可以扩大应用的内存占用范围,但是这个技术必须谨慎使用,绝大多数应用都不应该贸然使用多进程,一方面是因为使用多进程会使得代码逻辑更加复杂,另外如果使用不当,它可能反而会导致显著的内存增加。当你的应用需要运行一个常驻后台的任务,而且这个任务并不轻量,可以考虑使用这个技术,一个典型的例子是创建一个可以长时间后台播放的 Music Player。如果整个应用都运行在一个进程中,当后台播放的时候,前台的那些 UI 资源也没有办法得到释放,类似这样的应用可以切分成两个进程:一个用来操作 UI,另外一个给后台的 Service。

    引用

    http://blog.csdn.net/luoshengyang/article/details/42555483
    http://blog.csdn.net/luoshengyang/article/details/41688319
    http://blog.csdn.net/luoshengyang/article/details/42492621
    http://blog.csdn.net/luoshengyang/article/details/41338251
    http://blog.csdn.net/luoshengyang/article/details/41581063
    https://mp.weixin.qq.com/s?__biz=MzA4MzEwOTkyMQ==&mid=2667377215&idx=1&sn=26e3e9ec5f4cf3e7ed1e90a0790cc071&chksm=84f32371b384aa67166a3ff60e3f8ffdfbeed17b4c8b46b538d5a3eec524c9d0bcac33951a1a&scene=0&key=c2240201df732cf062d22d3cf95164740442d817864520af90bb0e71fa51102f2e91475a4f597ec20653c59d305c8a3e518d3f575d419dfcf8fb63a776e0d9fa6d3a9a6a52e84fedf3f467fe4af1ba8b&ascene=0&uin=Mjg5MDI3NjQ2Mg%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.12.3+build(16D32)&version=12010310&nettype=WIFI&fontScale=100&pass_ticket=Upl17Ws6QQsmZSia%2F%2B0xkZs9DYxAJBQicqh8rcaxYUjcu3ztlJUPxYrQKML%2BUtuf
    http://geek.csdn.net/news/detail/127226
    http://www.jianshu.com/p/216b03c22bb8
    https://zhuanlan.zhihu.com/p/25213586
    https://joyrun.github.io/2016/08/08/AndroidMemoryLeak/
    http://www.cnblogs.com/larack/p/6071209.html
    https://source.android.com/devices/tech/dalvik/gc-debug.html
    http://blog.csdn.net/high2011/article/details/53138202
    http://gityuan.com/2015/10/03/Android-GC/
    http://www.ayqy.net/blog/android-gc-log%E8%A7%A3%E8%AF%BB/
    https://developer.android.com/studio/profile/investigate-ram.html
    https://zhuanlan.zhihu.com/p/26043999
    http://www.csdn.net/article/2015-09-18/2825737

    展开全文
  • Android-性能优化-内存优化

    千次阅读 2017-09-22 11:16:56
    Android-性能优化-内存优化 概述 JVM 内存分配机制 详见:JVM 内存分配机制 JVM 垃圾回收机制 详见:JVM 垃圾回收机制 DVM 与 JVM 的区别 虚拟机区别 Dalvik 虚拟机(DVM)是 Android 系统在 ...

    Android-性能优化-内存优化

    相关系列

    概述

    JVM 内存分配机制

    JVM 垃圾回收机制

    DVM 与 JVM 的区别

    • 虚拟机区别

    Dalvik 虚拟机(DVM)是 Android 系统在 java虚拟机(JVM)基础上优化得到的,DVM 是基于寄存器的,而 JVM 是基于栈的,由于寄存器高效快速的特性,DVM 的性能相比 JVM 更好。

    • 字节码区别

    Dalvik 执行 .dex 格式的字节码文件,JVM 执行的是 .class 格式的字节码文件,Android 程序在编译之后产生的 .class 文件会被 aapt 工具处理生成 R.class 等文件,然后 dx 工具会把 .class 文件处理成 .dex 文件,最终资源文件和 .dex 文件等打包成 .apk 文件。

    OOM 代码相关优化

    当应用程序申请的 java heap 空间超过 Dalvik VM HeapGrowthLimit 时溢出。 OOM 并不代表内存不足,只要申请的 heap 超过 Dalvik VM HeapGrowthLimit 时,即使内存充足也会溢出。 效果是能让较多进程常驻内存。

    • Bitmap

    Bitmap 非常消耗内存,而且在 Android 中,读取 bitmap 时, 一般分配给虚拟机的图片堆栈只有 8M,所以经常造成 OOM 问题。 所以有必要针对 Bitmap 的使用作出优化:

    1. 图片显示:加载合适尺寸的图片,比如显示缩略图的地方不要加载大图。
    2. 图片回收:使用完 bitmap,及时使用 Bitmap.recycle() 回收。

    问题:Android 不是自身具备垃圾回收机制吗?此处为何要手动回收?

    Bitmap 对象不是 new 生成的,而是通过 BitmapFactory 生产的。 而且通过源码可发现是通过调用 JNI 生成 Bitma p对象(nativeDecodeStream()等方法)。 所以,加载 bitmap 到内存里包括两部分,Dalvik 内存和 Linux kernel 内存。 前者会被虚拟机自动回收。 而后者必须通过 recycle() 方法,内部调用 nativeRecycle() 让 linux kernel 回收。

    1. 捕获 OOM 异常:程序中设定如果发生 OOM 的应急处理方式。
    2. 图片缓存:内存缓存、硬盘缓存等
    3. 图片压缩:直接使用 ImageView 显示 Bitmap 时会占很多资源,尤其当图片较大时容易发 生OOM。 可以使用 BitMapFactory.Options 对图片进行压缩。
    4. 图片像素:android 默认颜色模式为 ARGB_8888,显示质量最高,占用内存最大。 若要求不高时可采用 RGB_565 等模式。
    5. 图片大小:图片 长度×宽度×单位像素 所占据字节数。

    我们知道 ARGB 指的是一种色彩模式,里面 A 代表 Alpha,R 表示 Red,G 表示 Green,B 表示 Blue。 所有的可见色都是由红绿蓝组成的,所以红绿蓝又称为三原色,每个原色都存储着所表示颜色的信息值,下表中对四种颜色模式的详细描述,以及每种色彩模式占用的字节数。

    模式描述占用字节
    ALPHAAlpha 由 8 位组成1B
    ARGB_44444 个 4 位组成 16 位,每个色彩元素站 4 位2B
    ARGB_88884 个 8 为组成 32 位,每个色彩元素站 8 位(默认)4B
    RGB_565R 为 5 位,G 为 6 位,B 为 5 位共 16 位,没有Alpha2B
    • 对象引用类型
    1. 强引用(Strong Reference):JVM宁愿抛出OOM,也不会让GC回收的对象
    2. 软引用(Soft Reference) :只有内存不足时,才会被GC回收。
    3. 弱引用(weak Reference):在GC时,一旦发现弱引用,立即回收
    4. 虚引用(Phantom Reference):任何时候都可以被 GC 回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。 程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。 可以用来作为 GC 回收 Object 的标志。
    • 缓存池

    对象池:如果某个对象在创建时,需要较大的资源开销,那么可以将其放入对象池,即将对象保存起来,下次需要时直接取出使用,而不用再次创建对象。当然,维护对象池也需要一定开销,故要衡量。

    线程池:与对象池差不多,将线程对象放在池中供反复使用,减少反复创建线程的开销。

    内存泄露相关优化

    当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

    • 单例造成的内存泄漏

    单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏,由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。

    如下这个典例:

    public class AppManager {
        private static AppManager instance;
        private Context context;
        private AppManager(Context context) {
            this.context = context;
        }
        public static AppManager getInstance(Context context) {
            if (instance != null) {
                instance = new AppManager(context);
            }
            return instance;
        }
    }
    

    这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个 Context,所以这个 Context 的生命周期的长短至关重要:

    1. 传入的是 Application 的 Context:这将没有任何问题,因为单例的生命周期和 Application 的一样长。
    2. 传入的是 Activity 的 Context:当这个 Context 所对应的 Activity 退出时,由于该 Context 和 Activity 的生命周期一样长(Activity 间接继承于 Context),所以当前 Activity 退出时它的内存并不会被回收,因为单例对象持有该 Activity 的引用。

    所以正确的单例应该修改为下面这种方式:

    public class AppManager {
        private static AppManager instance;
        private Context context;
        private AppManager(Context context) {
            this.context = context.getApplicationContext();
        }
        public static AppManager getInstance(Context context) {
            if (instance != null) {
                instance = new AppManager(context);
            }
            return instance;
        }
    }
    

    这样不管传入什么 Context 最终将使用 Application 的 Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。

    • 非静态内部类创建静态实例造成的内存泄漏

    有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:

    public class MainActivity extends AppCompatActivity {
        private static TestResource mResource = null;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            if(mResource == null){
                mResource = new TestResource();
            }
            //...
        }
        class TestResource {
        //...
        }
    }
    

    这样就在 Activity 内部创建了一个非静态内部类的单例,每次启动 Activity 时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该 Activity 的引用,导致 Activity 的内存资源不能正常回收。

    正确的做法为:

    将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用 Context,请使用 ApplicationContext。

    • Handler 造成的内存泄漏

    Handler 的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等 api 都应该会借助 Handler 来处理,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏,如下示例:

    public class MainActivity extends AppCompatActivity {
    	private Handler mHandler = new Handler() {
    	    @Override
    	    public void handleMessage(Message msg) {
    	    //...
    	    }
    	};
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            loadData();
        }
        private void loadData(){
            //...request
            Message message = Message.obtain();
            mHandler.sendMessage(message);
        }
    }
    

    这种创建 Handler 的方式会造成内存泄漏,由于 mHandler 是 Handler 的非静态匿名内部类的实例,所以它持有外部类 Activity 的引用,我们知道消息队列是在一个 Looper 线程中不断轮询处理消息,那么当这个 Activity 退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的 Message 持有 mHandler 实例的引用,mHandler 又持有 Activity 的引用,所以导致该 Activity 的内存资源无法及时回收,引发内存泄漏,所以另外一种做法为:

    public class MainActivity extends AppCompatActivity {
        private MyHandler mHandler = new MyHandler(this);
        private TextView mTextView ;
        private static class MyHandler extends Handler {
            private WeakReference<Context> reference;
            public MyHandler(Context context) {
            reference = new WeakReference<>(context);
            }
            @Override
            public void handleMessage(Message msg) {
                MainActivity activity = (MainActivity) reference.get();
                if(activity != null){
                activity.mTextView.setText("");
                }
            }
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mTextView = (TextView)findViewById(R.id.textview);
            loadData();
        }
    
        private void loadData() {
            //...request
            Message message = Message.obtain();
            mHandler.sendMessage(message);
        }
    }
    

    创建一个静态 Handler 内部类,然后对 Handler 持有的对象使用弱引用,这样在回收时也可以回收 Handler 持有的对象,这样虽然避免了 Activity 泄漏,不过 Looper 线程的消息队列中还是可能会有待处理的消息,所以我们在 Activity 的 Destroy 时或者 Stop 时应该移除消息队列中的消息,更准确的做法如下:

    public class MainActivity extends AppCompatActivity {
        private MyHandler mHandler = new MyHandler(this);
        private TextView mTextView ;
        private static class MyHandler extends Handler {
            private WeakReference<Context> reference;
            public MyHandler(Context context) {
            reference = new WeakReference<>(context);
            }
            @Override
            public void handleMessage(Message msg) {
                MainActivity activity = (MainActivity) reference.get();
                if(activity != null){
                activity.mTextView.setText("");
                }
            }
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mTextView = (TextView)findViewById(R.id.textview);
            loadData();
        }
    
        private void loadData() {
            //...request
            Message message = Message.obtain();
            mHandler.sendMessage(message);
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            mHandler.removeCallbacksAndMessages(null);
        }
    }
    

    使用 mHandler.removeCallbacksAndMessages(null); 是移除消息队列中所有消息和所有的 Runnable。 当然也可以使用 mHandler.removeCallbacks(); 或 mHandler.removeMessages(); 来移除指定的 Runnable 和 Message。

    • 线程造成的内存泄漏

    对于线程造成的内存泄漏,也是平时比较常见的,异步任务和 Runnable 都是一个匿名内部类,因此它们对当前 Activity 都有一个隐式引用。 如果 Activity 在销毁之前,任务还未完成,那么将导致 Activity 的内存资源无法回收,造成内存泄漏。 正确的做法还是使用静态内部类的方式,如下:

    static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        private WeakReference<Context> weakReference;
    
        public MyAsyncTask(Context context) {
            weakReference = new WeakReference<>(context);
        }
    
        @Override
        protected Void doInBackground(Void... params) {
            SystemClock.sleep(10000);
            return null;
        }
    
        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            MainActivity activity = (MainActivity) weakReference.get();
            if (activity != null) {
            //...
            }
        }
    }
    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            SystemClock.sleep(10000);
        }
    }
    //——————
    new Thread(new MyRunnable()).start();
    new MyAsyncTask(this).execute();
    

    这样就避免了 Activity 的内存资源泄漏,当然在 Activity 销毁时候也应该取消相应的任务 AsyncTask::cancel(),避免任务在后台执行浪费资源。

    • 资源使用完未关闭

    BraodcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。

    不要直接对 Activity 进行直接引用作为成员变量,如果不得不这么做,请用 private WeakReference mActivity 来做,相同的,对于Service 等其他有自己声明周期的对象来说,直接引用都需要谨慎考虑是否会存在内存泄露的可能。

    其他优化

    • 常用数据结构优化
    1. ArrayMap 及 SparseArray 是 android 的系统 API,是专门为移动设备而定制的。 用于在一定情况下取代 HashMap 而达到节省内存的目的。 对于 key 为 int 的 HashMap 尽量使用 SparceArray 替代,大概可以省 30% 的内存,而对于其他类型,ArrayMap 对内存的节省实际并不明显,10% 左右,但是数据量在 1000 以上时,查找速度可能会变慢。
    2. 在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用 StringBuilder 来替代频繁的 “+”。
    • 枚举

    Android 平台上枚举是比较争议的,在较早的 Android 版本,使用枚举会导致包过大,使用枚举甚至比直接使用 int 包的 size 大了 10 多倍。 在 stackoverflow 上也有很多的讨论, 大致意思是随着虚拟机的优化,目前枚举变量在 Android 平台性能问题已经不大,而目前 Android 官方建议,使用枚举变量还是需要谨慎,因为枚举变量可能比直接用 int 多使用 2 倍的内存。

    • View 复用
    1. 使用 ListView 时 getView 里尽量复用 conertView,同时因为 getView 会频繁调用,要避免频繁地生成对象。 优先考虑使用 RecyclerView 代替 ListView。
    2. 重复的布局优先使用 ,使用 减少 view 的层级,对于可以延迟初始化的页面,使用 。
    • 谨慎使用多进程

    现在很多 App 都不是单进程,为了保活,或者提高稳定性都会进行一些进程拆分,而实际上即使是空进程也会占用内存(1M 左右),对于使用完的进程,服务都要及时进行回收。

    • 系统资源

    尽量使用系统组件,图片甚至控件的 id。 例如:@android:color/xxx,@android:style/xxx。

    使用工具检查内存泄漏

    即使在编码时将上述情况都考虑了,往往会有疏忽的地方,更何况通常情况下是团队开发。 所以不仅仅要在编码时考虑内存优化的情况,当出现内存泄漏时,更有效更准确的定位问题才是最重要的方式。 内存泄漏不像 bug,排查起来相对复杂一些,下面介绍下常用的检查方式。

    使用 Lint 代码静态检查

    Lint 是 Android Studio 自带的工具,使用很简单找到 Analyze -> Inspect Code 然后选择想要扫面的区域即可。

    这里写图片描述

    选择 Lint 扫描区域。

    这里写图片描述

    对可能引起性能问题的代码,Lint 都会进行提示。

    这里写图片描述

    使用 Android Studio 自带的 Monitor Memory 检查

    一般在 Android Studio 的底部可以找到 Android Monitor。

    这里写图片描述

    可以看到当前 App的内存变动比较大,很有可能出现了内存泄漏。 点击 Dump Java Heap,等一段时间会自动生成 Heap Snapshot 文件。

    这里写图片描述

    在 Captures 中可以找到 hprof 文件。

    这里写图片描述

    在右侧找到 Analyzer Tasks 并打开,点击图中 Perform Analysis 按钮开始分析。

    这里写图片描述

    通过分析结果可以看到 TestActivity 泄漏了,从左侧 Reference Tree 中可以看到是 TestActivity 中的 context 泄露了。

    这里写图片描述

    我们来看下代码:

    public class TestActivity extends AppCompatActivity {
    
        private static Context context;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_test);
    
            context = this;
    
        }
    }
    

    代码中 context 为静态的引用了当前 Activity 造成了当前 Activity 无法释放。

    一般的通过 使用 Android Studio 自带的 Monitor Memory 可以定位到内存泄漏所在的类,更详细的信息需要借助 Memory Analyzer Tool(MAT)工具。

    使用 Memory Analyzer Tool 检查

    首先下载 Memory Analyzer Tool 下载地址

    在 Android Studio 中先将 hprof 文件导出为 MAT 可以识别的 hprof 文件。

    这里写图片描述

    打开刚才导出的文件。

    这里写图片描述

    经过分析后会显示如下,Leak Suspectss 是一个关于内存泄露猜想的饼图,Problem Suspect 1 是泄露猜想的描述。

    这里写图片描述

    Overview 是一个概况图,把内存的消耗以饼状图形式显示出来,鼠标在每个饼块区域划过或者点击,就会在 Inspector 栏目显示这块区域的相关信息。 MAT 从多角度提供了内存分析,其中包括 Histogram、 Dominator Tree、 Leak Suspects 和 Top consumers 等。
    这里写图片描述

    这里我们使用 Histogram 进行分析,切换到 Histogram 页面。 这个页面主要有 4 个列,Class Name、 Objects、 Shallow Heap 和 Retained Heap。 其中 Class Name 是全类名,Objects 是这个类的对象实例个数。 Shallow Heap 是对象本身占用内存大小,非数组的常规对象,本身内存占用很小,所以这个对泄露分析作用不大。 Retained Heap 指当前对象大小和当前对象能直接或间接引用的对象大小的总和,这个栏目是分析重点。

    这里写图片描述

    内存分析是分析的整个系统的内存泄露,而我们只要查找我们 App 的内存泄露情况。 这无疑增加了很多工作,不过幸亏 Histogram 支持正则表达式查找,在 Regex 中输入我们的包名进行过滤,直奔和我们 App 有关的内存泄露。

    这里写图片描述

    过滤后就显示了我们 App 相关内存信息,按 Retained Heap 大小排列下,发现 MainActivity 和 TestActivity 这两个类问题比较大。 TestActivity 的问题更突出些,所以先从 TestActivity 下手。

    首先看下是哪里的引用导致了 TestActivity 不能被 GC 回收。 右键使用 Merge Shortest Paths to GC Roots 显示距 GC Root 最短路径,当然选择过程中要排除软引用和弱引用,因为这些标记的一般都是可以被回收的。

    这里写图片描述

    进入结果页查看。

    这里写图片描述

    可以看到 TestActivity 不能被 GC 回收是因为 context 没有释放的原因。 我们再来看下代码:

    public class TestActivity extends AppCompatActivity {
    
        private static Context context;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_test);
    
            context = this;
    
        }
    }
    

    使用 LeakCanary 检查

    项目地址:https://github.com/square/leakcanary

    使用方式很简单,参考项目里面的介绍即可。

    这里写图片描述

    ANR

    • 什么是 ANR
    1. ANR:Application Not Responding,即应用无响应。
    2. 为用户在主线程长时间被阻塞是提供交互,提高用户体验。
    3. Android 系统自身的一种检测机制。
    • ANR 的类型

    ANR 一般有三种类型:

    1. KeyDispatchTimeout(5 seconds) : 主要类型按键或触摸事件在特定时间内无响应
    2. BroadcastTimeout(10 seconds) : BroadcastReceiver 在特定时间内无法处理完成
    3. ServiceTimeout(20 seconds) : 小概率类型 Service 在特定的时间内无法处理完成
    • ANR 产生的原因

    超时时间的计数一般是从按键分发给 app 开始。 超时的原因一般有两种:

    1. 当前的事件没有机会得到处理(即 UI 线程正在处理前一个事件,没有及时的完成或者 looper 被某种原因阻塞住了)
    2. 当前的事件正在处理,但没有及时完成。
    • ANR 出现流程分析
    1. 输入时间响应超时导致ANR流程

    在系统输入管理服务进程(InputManagerService)中有一个线程(InputDispathcerThread)专门管理输入事件的分发,在该线程处理输入事件的过程中,回调用 InputDispatcher 对象方法不断的检测处理过程是否超时,一旦超时,则会通过一些列的回调调用 InputMethod 对象的 notifyANR 方法,其会最终出发 AMS 中 handler 对象的 SHOW_NOT_RESPONDING_MSG 这个事件,显示ANR对话框。

    1. 广播发生ANR流程

    广播分为三类:普通的,有序的,异步的。 只有有序(ordered)的广播才会发生超时,而在 AndroidManifest 中注册的广播都会被当做有序广播来处理,会被放在广播的队列中串行处理。 AMS 在处理广播队列时,会设置一个超时时间,当处理一个广播达到超时时间的限制时,就会触发 BroadcastQueue 类对象的 processNextBroadcast 方法来判断是否超时,如果超时,就会终止该广播,触发ANR对话框。

    1. UI线程

    UI 线程主要包括如下:

    Activity : onCreate(), onResume(), onDestroy(), onKeyDown(), onClick(), etc 生命周期方法里。
    AsyncTask : onPreExecute(), onProgressUpdate(), onPostExecute(), onCancel, etc 这些异步更改 UI 界面的方法里。
    Mainthread handler : handleMessage(), post*(runnable r), getMainLooper(), etc 通过 handler 发送消息到主线程的 looper,即占用主线程 looper 的。

    • ANR 执行流程

    了解 ANR 执行流程有利于我们制定 ANR 监控策略和获取 ANR 的相关信息,ANR 的执行步骤如下:

    1. 系统捕获到 ANR 发生;
    2. Process 依次向本进程及其他正在运行的进程发送 Linux 信号量 3;
    3. 进程接收到 Linux 信号量,并向 /data/anr/traces.txt 中写入进程信息;
    4. Log 日志打印 ANR 信息;
    5. 进程进入 ANR 状态(此时可以获取到进程 ANR 信息);
    6. 弹出 ANR 提示框;
    7. 提示框消失,进程回归正常状态。

    由于向 /data/anr/traces.txt 文件中写入信息耗时较长,从 Input ANR 触发到弹出 ANR 提示框一般在 10s 左右(不同 rom 时间不同)。

    • 发生 ANR 如何定位

    当 App 的进程发生 ANR 时,系统让活跃的 Top 进程都进行了一下 dump,进程中的各种 Thread 就都 dump 到这个 trace 文件里了,所以 trace 文件中包含了每一条线程的运行时状态。 traces.txt 的文件放在 /data/anr/ 下. 可以通过 adb 命令将其导出到本地:

    $ adb pull data/anr/traces.txt .
    

    通过分析 traces.txt 文件,查找 App 包名关键信息来定位 ANR。

    参考资料

    Android Bitmap的内存大小是如何计算的?

    Android性能优化之常见的内存泄漏

    使用新版Android Studio检测内存泄露和性能

    Android 应用内存泄漏的定位、分析与解决策略

    Android 系统稳定性 - ANR

    其他系列

    Gradle 系列

    更多文章:

    这是我博客长期更新的项目,欢迎大家 Star。
    https://github.com/jeanboydev/Android-ReadTheFuckingSourceCode

    我的公众号

    欢迎你「扫一扫」下面的二维码,关注我的公众号,可以接受最新的文章推送,有丰厚的抽奖活动和福利等着你哦!?

    如果你有什么疑问或者问题,可以 点击这里 提交 issue,也可以发邮件给我 jeanboy@foxmail.com

    同时欢迎你 Android技术进阶:386463747 来一起交流学习,群里有很多大牛和学习资料,相信一定能帮助到你!

    展开全文
  • 上篇博客我们写到了 Java/Android 内存的分配以及相关 GC 的详细分析,这篇博客我们会继续分析 Android 中内存泄漏的检测以及相关案例,和 Android 的内存优化相关内容。 Android 内存泄漏案例和检测   常见...

    上篇博客我们写到了 Java/Android 内存的分配以及相关 GC 的详细分析,这篇博客我们会继续分析 Android 中内存泄漏的检测以及相关案例,和 Android 的内存优化相关内容。
      上篇:Android 性能优化之内存泄漏检测以及内存优化(上)
      中篇:Android 性能优化之内存泄漏检测以及内存优化(中)
      下篇:Android 性能优化之内存泄漏检测以及内存优化(下)
      转载请注明出处:http://blog.csdn.net/self_study/article/details/66969064
      对技术感兴趣的同鞋加群544645972一起交流。

    Android 内存泄漏检测

    通过上篇博客我们了解了 Android JVM/ART 内存的相关知识和泄漏的原因,再来归类一下内存泄漏的源头,这里我们简单将其归为一下三类:

    • 自身编码引起
    • 由项目开发人员自身的编码造成;
    • 第三方代码引起
    • 这里的第三方代码包含两类,第三方非开源的 SDK 和开源的第三方框架;
    • 系统原因
    • 由 Android 系统自身造成的泄漏,如像 WebView、InputMethodManager 等引起的问题,还有某些第三方 ROM 存在的问题。

    Android 内存泄漏的定位,检测与修复

    内存泄漏不像闪退的 BUG,排查起来相对要困难一些,比较极端的情况是当你的应用 OOM 才发现存在内存泄漏问题,到了这种情况才去排查处理问题的话,对用户的影响就太大了,为此我们应该在编码阶段尽早地发现问题,而不是拖到上线之后去影响用户体验,下面总结一下常用内存泄漏的定位和检测工具:

    Lint

    Lint 是 Android studio 自带的静态代码分析工具,使用起来也很方便,选中需要扫描的 module,然后点击顶部菜单栏 Analyze -> Inspect Code ,选择需要扫描的地方即可:
    这里写图片描述      这里写图片描述
    这里写图片描述
    最后在 Performance 里面有一项是 Handler reference leaks,里面列出来了可能由于内部 Handler 对象持有外部 Activity 引用导致内存泄漏的地方,这些地方都可以根据实际的使用场景去排查一下,因为毕竟不是每个内部 Handler 对象都会导致内存泄漏。Lint 还可以自定义扫描规则,使用姿势很多很强大,感兴趣的可以去了解一下,除了 Lint 之外,还有像 FindBugs、Checkstyle 等静态代码分析工具也是很不错的。

    StrictMode

    StrictMode 是 Android 系统提供的 API,在开发环境下引入可以更早的暴露发现问题给开发者,于开发阶段解决它,StrictMode 最常被使用来检测在主线程中进行读写磁盘或者网络操作等耗时任务,把这些耗时任务放置于主线程会造成主线程阻塞卡顿甚至可能出现 ANR ,官方例子:

     public void onCreate() {
         if (DEVELOPER_MODE) {
             StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                     .detectDiskReads()
                     .detectDiskWrites()
                     .detectNetwork()   // or .detectAll() for all detectable problems
                     .penaltyLog()
                     .build());
             StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                     .detectLeakedSqlLiteObjects()
                     .detectLeakedClosableObjects()
                     .penaltyLog()
                     .penaltyDeath()
                     .build());
         }
         super.onCreate();
     }
    

    把上面这段代码放在早期初始化的 Application、Activity 或者其他应用组件的 onCreate 函数里面来启用 StrictMode 功能,一般 StrictMode 只是在测试环境下启用,到了线上环境就不要开启这个功能。启用 StrictMode 之后,在 logcat 过滤日志的地方加上 StrictMode 的过滤 tag,如果发现一堆红色告警的 log,说明可能就出现了内存泄漏或者其他的相关问题了:
    这里写图片描述
    比如上面这个就是因为调用 registerReceiver 之后忘记调用 unRegisterReceiver 导致的 activity 泄漏,根据错误信息便可以定位和修复问题。

    LeakCanary

    LeakCanary 是一个 Android 内存泄漏检测的神器,正确使用可以大大减少内存泄漏和 OOM 问题,地址:

    https://github.com/square/leakcanary
    

    集成 LeakCanary 也很简单,在 build.gradle 文件中加入:

    dependencies {
       debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
       releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
       testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
     }
    

    然后在 Application 类中添加下面代码:

    public class ExampleApplication extends Application {
    
      @Override public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
          // This process is dedicated to LeakCanary for heap analysis.
          // You should not init your app in this process.
          return;
        }
        LeakCanary.install(this);
        // Normal app init code...
      }
    }
    

    上面两步做完之后就算是集成了 LeakCanary 了,非常简单方便,如果程序出现了内存泄漏会弹出 notification,点击这个 notification 就会进入到下面这个界面,或者集成 LeakCanary 之后在桌面会有一个 LeakCanary 的图标,点击进去是所有的内存泄漏列表,点击其中一项同样是进入到下面界面:
    这里写图片描述
    这个界面就会详细展示引用持有链,一目了然,对于问题的解决方便了很多,堪称神器,更多实用姿势可以看看 LeakCanary FAQ
      还有一点需要提到的是,LeakCanary 在检测内存泄漏的时候会阻塞主界面,这是一点体验有点不爽的地方,但是这时候阻塞肯定是必要的,因为此时必须要挂起线程来获取当前堆的状态。然后也并不是每个 LeakCanary 提示的地方都有内存泄漏,这时候可能需要借助 MAT 等工具去具体分析。不过 LeakCanary 有一点非常好的地方是因为 Android 系统也会有一些内存泄漏,而 LeakCanary 对此则提供了一个 AndroidExcludedRefs 类来帮助我们排除这些问题。

    adb shell && Memory Usage

    可以通过命令 adb shell dumpsys meminfo [package name][-d] 来将指定 package name 的内存信息打印出来,-d 选项会额外打印信息,数据单位为 KB,这种模式可以通过 Activities 选项非常直观地看到 Activity 未释放导致的内存泄漏:
    在这里插入图片描述
    一般情况下我们只需要关心 PSS Total + Private Dirty。相关字段的含义如下:

    ColumnDescription
    Private Dirty/Clean这是你指定应用单独占用的内存数量,换句话说这个就是你应用完全被销毁之后系统可以回收的内存数。一般来说,最重要的部分就是 Privity Dirty 部分,因为这个是你指定进程单独占用的内存数,而且 Android 没有使用 Swap 策略(交换分区),所以这部分内存只能存在于 RAM 中。所有应用创建的 Dalvik 和 Native 堆都是 Private Dirty RAM,和 Zygote 进程共享的 Dalvik 和 Native 堆都是 Shared Dirty RAM。Private Dirty/Clean 的相同之处在于他们都只是被当前这个进程引用,区别在于 Clean 是内存中的页面一直没有被该进程写入修改(映射持久文件使用的内存页,例如 so 和 dex,因此如果内存紧张的话可能会被置换出去,下次再使用的时候重新读入即可)而 Dirty 则进行了写入修改,所以 Private Clean 部分内存进行写入操作之后就会被挪到 Private Dirty 中
    Proportional Set Size (Pss)这个值是在应用所有占用 RAM 的基础上增加和其他应用进程共享的内存(比如同一个动态链接库被三个应用使用到,那么每个应用的 PSS 都会增加 so.size/3),如果只有你一个应用在使用这部分内存,那么这部分内存将直接加到 PSS 上。绝大多数情况下 PSS 都是大于等于 Private Dirty/Clean 数值
    Swap Dirty某些 Android 设备可以开启 Swap 策略,Linux 下又一个策略可以把内存压缩之后交换到 RAM 里面的一个特殊区域,当使用之后又会把它解压缩之后重新交换进内存
    Heap Size虚拟机分配的堆大小,数值上基本等于 Heap Alloc(已经分配的部分)+ Heap Free(可用部分)
    Heap Alloc统计的是虚拟机分配的所有应用实例的内存,也会将引用从 Zygote 共享的部分算进去,所以导致此值大于 Pss Total 和 Private Dirty
    Dalvik HeapDalvik 虚拟机使用的内存,Pss 包括了和其他应用共有的部分,而 Private Dirty 部分则是包含了应用自己分配和已经修改的 Zygote Allocation 页。包含 /dev/ashmem/dalvik-main space, /dev/ashmem/dalvik-allocspace, /dev/ashmem/dalvik-large object space, /dev/ashmem/dalvik-zygote space, /dev/ashmem/dalvik-non moving space的 Pss 部分
    Dalvik Other用于存放类的数据结构及关系以及管理 Dalvik 的内存(如, just-in-time compilation (JIT) and GC bookkeeping),随着类和函数数量,代码复杂度的增加而增加,除了 Dalvik Heap 外其他的以 /dev/ashmem/dalvik-*开头的空间。
    Ashmem共享内存
    Other dev除了在 Dalvik Heap、Dalvik Other 、Ashmem 列出内存之外(5.1 版本之后也剔除了 /dev/kgsl-3d0 )其他以/dev/* 开头的内存区域
    .so && .dex mmap.so mmap 和 .dex mmap 是用来映射 so 和 dex 代码的内存,Pss Total 包含了和其他应用共享的框架代码和动态库,Private Clean 则是应用单独的代码部分。通常情况下实际映射的内存会更大,因为当前只加载了已经执行的代码在内存中。.so mmap 通常也会有 Private Dirty ,这是因为 so 库被加载到最终地址做了相关写入修改
    .oat && .art mmap代码映像占用的内存,它是基于通常情况下多个应用共同使用的预加载类,这部分内存是多个应用共享,不会被单个应用影响。尽管 .art mmap 映像包含 Object 实例,它仍然不会计入您的堆大小。
    EGL mtrack5.1 版本之前被称为Graphics, gralloc 分配的内存,主要是窗口系统,所有 Surface Buffer(4.1 版本之后开启了三倍缓冲,所以在没有 SurfaceView 和 TextureView 的情况下 EGL mtrack = 3 * 单个buffer的内存大小) 和 Atlas Buffer 的总和。按道理来说,Atlas Buffer 实际上是一个共享内存并且不应该被算入每个进程的内存占用里面,但是实际情况 Surface Buffer 和 Atlas Buffer 都被算入了,所以当评价一个应用的内存占用时,基本可以忽略该栏
    GL mtrack5.1 版本之前被称为 GL,驱动上报的GL内存使用情况。 和 Gfx dev 加一起主要是 GL texture 大小,GL command buffer,固定的全局驱动程序 RAM 开销等的总和。 这部分内存可以在/d/kgsl/proc/PID/mem里面查看。
    Gfx dev5.1 版本加入,/dev/kgsl-3d0 Pss 部分内存区域占用,和 GL mtrack 一样都是 GPU 驱动上报的内存大小。用户驱动空间和内核驱动空间共享一块内存区域,在某些 Android 设备上可能会重复计算两次从而造成 Gfx dev 比实际的要大一些
    Unknown系统无法将其分类到其他更具体的一个项中的任何 RAM 页。 其 Pss Total 与 Zygote共享

    在这里插入图片描述

    官方指引:https://developer.android.com/studio/command-line/dumpsys#meminfo
    其他资料:https://blog.csdn.net/msf568834002/article/details/78881341
         http://www.voidcn.com/article/p-wfedutyx-qx.html
         https://unity3d.com/cn/learn/tutorials/topics/best-practices/android-memory-management
         https://my.oschina.net/jerikc/blog/391907

    Android Memory Profiler

    Memory Profiler 是 Android Studio 自带的一个监控内存使用状态的工具,打开 Profiler 之后入口如下所示:
    在这里插入图片描述
    在 Android Profiler 点开之后 logcat 的右侧就是 Profiler 工具,其中可以检测内存、CPU、网络等内容,我们这里只用到了 Memory Profiler 功能,点击红色箭头所指的区域,就会 dump 此时此刻的 Memory 信息,并且生成一个 .hprof 文件,dump 完成之后会自动打开这个文件的显示界面,当然也可以点击最左侧的 Heap Dump 的保存按钮将 .hprof 文件保存下来,需要注意的是这个文件因为不是标准的Java SE HPROF 格式,所以如果需要在 MAT 上看就需要通过命令 hprof-conv heap-original.hprof heap-converted.hprof 进行转换:
    在这里插入图片描述
      接着我们来分析一下这个生成的 .hprof 文件所展示的信息:
    在这里插入图片描述
    首先左上角的下拉框,可以选择 App Heap、Image Heap 和 Zygote Heap,对应的就是上篇博客讲到的 Allocation Space,Image Space 和 Zygote Space,我们这里选择 Allocation Space,然后第二个选择 Arrange by package 这一项,展开之后就能看见一个树形结构了,然后继续展开我们应用包名的对应对象,就可以很清晰的看到有多少个 Activity 对象了,红框里面的信息解释如下:

    ColumnDescription
    JavaDalvik Heap。从 Java 或 Kotlin 代码分配的对象内存
    NativeNative Heap。从 C 或 C++ 代码分配的对象内存。即使应用中不使用 C++,也可能会看到此处使用的一些原生内存,因为 Android 框架使用原生内存代表您处理各种任务,如处理图像资源和其他图形时,即使只采用 Java 或 Kotlin 语言
    GraphicsGfxdev + EGL mtrack + GL mtrack。图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存(这是与 CPU 共享的内存,不是 GPU 专用内存)
    StackStack。应用中的原生堆栈和 Java 堆栈使用的内存。 这通常与应用运行多少线程有关
    Codeall mmaps。应用用于处理代码和资源(如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体)的内存
    OtherOther dev + Unknown。应用使用的系统不确定如何分类的内存
    Allocated应用分配的 Java/Kotlin 对象数,没有计入 C 或 C++ 中分配的对象
    ColumnDescription
    Allocations指定的堆中实例数
    Native SizeNative 层所有实例的总大小(以字节为单位)
    Shallow Size此堆中所有实例的总大小(以字节为单位)
    Retained Size为此类的所有实例而保留的内存总大小(以字节为单位),会大于等于 Shallow Size,代表该对象回收时可释放内存大小
    Depth从 GC Root 到该对象的引用链路的最短步数

    如果发现某个 Activity 对象存在多个,并且 GC 后也不会减少,那就大概率是该 Activity 泄漏了,点击展开 Refereces,从对象找到 GC Roots就可以找到是被谁强饮用无法释放了:
    这里写图片描述
    可以看到 Thread 对象持有了 SecondActivity 对象的引用,也就是 GC Root 持有了该 Activity 的引用,导致这个 Activity 无法回收,问题的根源我们就发现了,接下来去处理它就好了。
      Android Memory Profiler 除了上面提到的记录展示某个时刻应用内存占用详情的功能外,还能获取某段时间内应用的对象分配情况,使用方式也很简单,找一个 8.0 及之上的 Android 手机,在 Memory Profiler 时间线上拖动一个起始和结束即可:
    在这里插入图片描述
    这样下方的 Live Allocation 框里面就是应用内存实时分配的详情,里面四个选项分别代表:

    ColumnDescription
    Allocations这段时间之内指定的堆中分配的实例数
    DealLocations这段时间之内指定的堆中释放的实例数
    Total Count此堆中所有实例的数量
    Shallow Size此堆中所有实例的总大小(以字节为单位)

    MAT

    MAT(Memory Analyzer Tools)是一个 Eclipse 插件,它是一个快速、功能丰富的 JAVA heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗,MAT 插件的下载地址:Eclipse Memory Analyzer Open Source Project,上面通过 Android studio 生成的 .hprof 文件因为格式稍有不同,所以我们需要现将 AS 生成的 .hprof 文件保存到本地之后,通过 hprof-conv heap-original.hprof heap-converted.hprof 命令进行转换。通过 MAT 去打开转换之后的这个文件:
    这里写图片描述
    用的最多的就是 Histogram 功能,点击 Actions 下的 Histogram 项就可以得到 Histogram 结果:
    这里写图片描述
    我们可以在左上角写入一个正则表达式,然后就可以对所有的 Class Name 进行筛选了,很方便,顶栏展示的信息 “Objects” 代表该类名对象的数量,剩下的 “Shallow Heap” 和 “Retained Heap” 则和 Android Memory Profiler 类似。咱们接着点击 SecondActivity,然后右键:
    这里写图片描述
    在弹出来的菜单中选择 List objects->with incoming references 将该类的实例全部列出来:
    这里写图片描述
    通过这个列表我们可以看到 SecondActivity@0x12faa900 这个对象被一个 this$00x12c65140 的匿名内部类对象持有,然后展开这一项,发现这个对象是一个 handler 对象:
    这里写图片描述
    快速定位找到这个对象没有被释放的原因,可以右键 Path to GC Roots->exclude all phantom/weak/soft etc. references 来显示出这个对象到 GC Root 的引用链,因为强引用才会导致对象无法释放,所以这里我们要排除其他三种引用:
    这里写图片描述
    这么处理之后的结果就很明显了:
    这里写图片描述
    一个非常明显的强引用持有链,GC Root 我们前面的博客中说到包含了线程,所以这里的 Thread 对象 GC Root 持有了 SecondActivity 的引用,导致该 Activity 无法被释放。
      MAT 还有一个功能就是能够对比两个 .hprof 文件,将两个文件都添加到 Compare Basket 里面:
    这里写图片描述
    添加进去之后点击右上角的 ! 按钮,然后就会生成两个文件的对比:
    这里写图片描述
    同样适用正则表达式将需要的类筛选出来:
    这里写图片描述
    结果也很明显,退出 Activity 之后该 Activity 对象未被回收,仍然在内存中,或者可以调整对比选项让对比结果更加明显:
    这里写图片描述
    也可以对比两个对象集合,方法与此类似,都是将两个 Dump 结果中的对象集合添加到 Compare Basket 中去对比,找出差异后用 Histogram 查询的方法找出 GC Root,定位到具体的某个对象上。

    常见的内存泄漏案例

    我们来看看常见的导致内存泄漏的案例:

    静态变量造成的内存泄漏

    由于静态变量的生命周期和应用一样长,所以如果静态变量持有 Activity 或者 Activity 中 View 对象的应用,就会导致该静态变量一直直接或者间接持有 Activity 的引用,导致该 Activity 无法释放,从而引发内存泄漏,不过需要注意的是在大多数这种情况下由于静态变量只是持有了一个 Activity 的引用,所以导致的结果只是一个 Activity 对象未能在退出之后释放,这种问题一般不会导致 OOM 问题,只能通过上面介绍过的几种工具在开发中去观察发现。
      这种问题的解决思路很简单,就是不让静态变量直接或者间接持有 Activity 的强引用,可以将其修改为 soft reference 或者 weak reference 等等之类的,或者如果可以的话将 Activity Context 更换为 Application Context,这样就能保证生命周期一致不会导致内存泄漏的问题了。

    内部类持有外部类引用

    我们上面的 demo 中模拟的就是内部类对象持有外部类对象的引用导致外部类对象无法释放的问题,在 Java 中非静态内部类和匿名内部类会持有他们所属外部类对象的引用,如果这个非静态内部类对象或者匿名内部类对象被一个耗时的线程(或者其他 GC Root)直接或者间接的引用,甚至这些内部类对象本身就在做一些耗时操作,这样就会导致这个内部类对象直接或者间接无法释放,内部类对象无法释放,外部类的对象也就无法释放造成内存泄漏,而且如果无法释放的对象积累起来就会造成 OOM,示例代码如下所示:

    public class SecondActivity extends AppCompatActivity{
        private Handler handler;
        private Bitmap bitmap;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_second);
            bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);//decode 一个大图来模拟内存无法释放导致的崩溃
            findViewById(R.id.btn_second).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    finish();
                }
            });
    
            handler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
    
                }
            };
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    handler.sendEmptyMessage(0);
                }
            }).start();
        }
    }
    

    这个问题的解决方法可以根据实际情况进行选择:

    • 将非静态内部类或者匿名内部类修改为静态内部类,比如 Handler 修改为静态内部类,然后让 Handler 持有外部 Activity 的一个 Weak Reference 或者 Soft Reference;
    • 在 Activity 页面销毁的时候将耗时任务停止,这样就能保证 GC Root 不会间接持有 Activity 的引用,也就不会导致内存泄漏;

    错误使用 Activity Context

    这个很好理解,在一个错误的地方使用 Activity Context,造成 Activity Context 被静态变量长时间引用导致无法释放而引发的内存泄漏,这个问题的处理方式也很简单,如果可以的话修改为 Application Context 或者将强引用变成其他引用。

    资源对象没关闭造成的内存泄漏

    资源性对象比如(Cursor,File 文件等)往往都用了一些缓冲,我们在不使用的时候应该及时关闭它们,以便它们的缓冲对象被及时回收,这些缓冲不仅存在于 java 虚拟机内,还存在于 java 虚拟机外,如果我们仅仅是把它的引用设置为 null 而不关闭它们,往往会造成内存泄漏。但是有些资源性对象,比如 SQLiteCursor(在析构函数 finalize(),如果我们没有关闭它,它自己会调 close() 关闭),如果我们没有关闭它系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的 close() 函数,将其关闭掉,然后再置为 null,在我们的程序退出时一定要确保我们的资源性对象已经关闭。
      程序中经常会进行查询数据库的操作,但是经常会有使用完毕 Cursor 后没有关闭的情况,如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会出现内存问题,这样就会给以后的测试和问题排查带来困难和风险,示例代码:

    Cursor cursor = getContentResolver().query(uri...); 
    if (cursor.moveToNext()) { 
    ... ... 
    } 
    

    更正代码:

    Cursor cursor = null;
    try {
        cursor = getContentResolver().query(uri...);
        if (cursor != null && cursor.moveToNext()) {
            ... ...
        }
    } finally {
        if (cursor != null) {
            try {
                cursor.close();
            } catch (Exception e) {
                //ignore this
            }
        }
    }
    

    集合中对象没清理造成的内存泄漏

    在实际开发过程中难免会有把对象添加到集合容器(比如 ArrayList)中的需求,如果在一个对象使用结束之后未将该对象从该容器中移除掉,就会造成该对象不能被正确回收,从而造成内存泄漏,解决办法当然就是在使用完之后将该对象从容器中移除。

    WebView造成的内存泄露

    具体的可以看看我的这篇博客:android WebView详解,常见漏洞详解和安全源码(下)

    未取消注册导致的内存泄漏

    一些 Android 程序可能引用我们的 Android 程序的对象(比如注册机制),即使我们的 Android 程序已经结束了,但是别的应用程序仍然还持有对我们 Android 程序某个对象的引用,这样也会造成内存不能被回收,比如调用 registerReceiver 后未调用unregisterReceiver。假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息,则可以在 LockScreen 中定义一个 PhoneStateListener 的对象,同时将它注册到 TelephonyManager 服务中,对于 LockScreen 对象,当需要显示锁屏界面的时候就会创建一个 LockScreen 对象,而当锁屏界面消失的时候 LockScreen 对象就会被释放掉,但是如果在释放 LockScreen 对象的时候忘记取消我们之前注册的 PhoneStateListener 对象,则会间接导致 LockScreen 无法被回收,如果不断的使锁屏界面显示和消失,则最终会由于大量的 LockScreen 对象没有办法被回收而引起 OOM,虽然有些系统程序本身好像是可以自动取消注册的(当然不及时),但是我们还是应该在程序结束时明确的取消注册。

    因为内存碎片导致分配内存不足

    还有一种情况是因为频繁的内存分配和释放,导致内存区域里面存在很多碎片,当这些碎片足够多,new 一个大对象的时候,所有的碎片中没有一个碎片足够大以分配给这个对象,但是所有的碎片空间加起来又是足够的时候,就会出现 OOM,而且这种 OOM 从某种意义上讲,是完全能够避免的。
      由于产生内存碎片的场景很多,从 Memory Profiler 来看,下面场景的内存抖动是很容易产生内存碎片的:
    这里写图片描述
    最常见产生内存抖动的例子就是在 ListView 的 getView 方法中未复用 convertView 导致 View 的频繁创建和释放,针对这个问题的处理方式那当然就是复用 convertView;或者是 String 拼接创建大量小的对象(比如在一些频繁调用的地方打字符串拼接的 log 的时候);如果是其他的问题,就需要通过 Memory Profiler 去观察内存的实时分配释放情况,找到内存抖动的地方修复它,或者如果当出现下面这种情况下的 OOM 时,也是由于内存碎片导致无法分配内存:
    这里写图片描述
    出现上面这种类型的 Crash 时就要去分析应用里面是不是存在大量分配释放对象的地方了。

    Android 内存优化

    内存优化请看下篇:Android 性能优化之内存泄漏检测以及内存优化(下)

    引用

    http://blog.csdn.net/luoshengyang/article/details/42555483
    http://blog.csdn.net/luoshengyang/article/details/41688319
    http://blog.csdn.net/luoshengyang/article/details/42492621
    http://blog.csdn.net/luoshengyang/article/details/41338251
    http://blog.csdn.net/luoshengyang/article/details/41581063
    https://mp.weixin.qq.com/s?__biz=MzA4MzEwOTkyMQ==&mid=2667377215&idx=1&sn=26e3e9ec5f4cf3e7ed1e90a0790cc071&chksm=84f32371b384aa67166a3ff60e3f8ffdfbeed17b4c8b46b538d5a3eec524c9d0bcac33951a1a&scene=0&key=c2240201df732cf062d22d3cf95164740442d817864520af90bb0e71fa51102f2e91475a4f597ec20653c59d305c8a3e518d3f575d419dfcf8fb63a776e0d9fa6d3a9a6a52e84fedf3f467fe4af1ba8b&ascene=0&uin=Mjg5MDI3NjQ2Mg%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.12.3+build(16D32)&version=12010310&nettype=WIFI&fontScale=100&pass_ticket=Upl17Ws6QQsmZSia%2F%2B0xkZs9DYxAJBQicqh8rcaxYUjcu3ztlJUPxYrQKML%2BUtuf
    http://geek.csdn.net/news/detail/127226
    http://www.jianshu.com/p/216b03c22bb8
    https://zhuanlan.zhihu.com/p/25213586
    https://joyrun.github.io/2016/08/08/AndroidMemoryLeak/
    http://www.cnblogs.com/larack/p/6071209.html
    https://source.android.com/devices/tech/dalvik/gc-debug.html
    http://blog.csdn.net/high2011/article/details/53138202
    http://gityuan.com/2015/10/03/Android-GC/
    http://www.ayqy.net/blog/android-gc-log解读/
    https://developer.android.com/studio/profile/investigate-ram.html
    https://zhuanlan.zhihu.com/p/26043999
    https://blog.csdn.net/msf568834002/article/details/78881341

    展开全文
  • Android 性能优化内存优化

    万次阅读 2016-12-18 20:33:11
    Android 性能优化内存优化 Android 应用程序在开发的过程中内存的准确控制是判断一个程序好坏的重要标准之一: 一、假如我们开发的程序内存溢出、泄漏了会引发那些实质性的问题呢?  1、程序卡顿、响应速度变慢。...
  • react native内存优化

    千次阅读 2017-02-14 16:29:05
    react native在加载大量图片时小伙伴们会发现,内存一路飙升150M-200M那都不是问题,今天就来优化一下内存,降至40M左右。 1:实现一个module  import android.app.ActivityManager; import android.content....
  • 在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但也随之带来了内存泄漏的可能
  • Redis内存优化

    千次阅读 2020-04-16 14:50:06
    使用maxmemory参数限制最大可用内存,当超出内存上限maxmemory时使用LRU等删除策略释放空间以及防止所用内存超过服务器物理内存。 2.配置内存回收策略 Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略...
  • 内存优化模块.ec内存优化模块.ec内存优化模块.ec内存优化模块.ec内存优化模块.ec内存优化模块.ec
  • iOS性能优化-内存优化

    千次阅读 2018-09-10 14:25:31
    一、为什么需要内存优化 二、内存管理 三、常见问题 四、内存占用 五、检测工具 摘要 一、为什么需要内存优化 The easy answer is users have a better experience. Not only will your app launch faster....
  • Android性能优化系列之内存优化

    万次阅读 2017-01-21 21:00:45
    GC)完成的,程序员不需要通过调用函数来释放内存,但也随之带来了内存泄漏的可能,上篇博客,我介绍了 Android性能优化系列之布局优化,本篇博客,我将介绍内存优化的相关知识。内存的分配策略概述程序运行时的内存...
  • Linux性能优化-内存性能优化思路

    千次阅读 2019-01-07 08:27:48
    内存优化思路
  • [Unity 优化]内存优化

    千次阅读 2017-11-07 22:21:40
    内存优化可以使用Unity profiler或者对应平台的内存分析工具进行分析,从 Unity3D内部的内存+Mono的托管内存 进行分析,避免内存泄漏。1、内存测试一般使用手机测试,Unity profiler在Editor测试下的结果不准确。...
  • 今天来聊一聊Android中内存优化的一些手段。 首先问问自己为什么要内存优化呢? (1):App消耗内存过大,导致手机内存低于内存警戒线的时候,Low Memory Killer机制就会触发,App占用内存越多,被处理掉的机会就越...
  • RocketMQ 内存优化

    千次阅读 2018-12-14 10:40:19
    RocketMQ 内存优化1.启动脚本的内存调整1.mqbroker和mqnamesrv的内存调整 RocketMQ 的默认内存占用非常高,是4×4g的,普通人是消耗不起的,所以第一件事情就是调整RocketMQ的占用内存。 调整RocketMQ的内存目前我所...
  • 布局优化   我们可以通过手机开发者选项中的调试GPU过度来查看布局绘制的复杂情况。 避免overdraw,使用RelativeLayout来替换掉多层LineraLayout嵌套 减少View树的层数,Google Api文档中建议View树...
  • KVM总结-KVM性能优化内存优化

    千次阅读 2017-05-07 17:28:20
    我们说完CPU方面的优化,接着继续内存方面的优化内存优化有以下四个方向去着手:1)EPT 技术、2)大页和透明大页、3)KSM 技术、4)内存限制
  • <?xml version="1.0" encoding="utf-8"?> <resources> <string name="__leak_canary_max_stored_leaks">20</string> </resources>
  • Android性能优化内存优化

    千次阅读 2019-01-29 11:24:06
    1.手机运行内存(RAM)就相当于我们电脑的内存,是手机中作为APP运行过程中临时性数据暂时存储的内存介质。 Android手机在出厂,虚拟机对单个应用可使用的最大内存做了限制,当你的应用使用内存超过这个限制就会产生...
  • 本文主要讲解性能优化中的内存优化,希望你们会喜欢 目录 1. 定义 优化处理 应用程序的内存使用、空间占用 2. 作用 避免因不正确使用内存 &amp; 缺乏管理,从而出现 内存泄露(ML)、内存...
  • Oracle彻底优化——优化内存

    千次阅读 2011-09-23 09:10:12
    内存的优化历来都是数据库或者操作系统优化的重中...优化内存主要是通过优化内存结构来提高系统性能。这里所说的内存结构主要由专用SQL及PL/SQL区、共享池、日志缓冲区和高速缓冲存储区构成。 由于0racle的内存结构需
  • 优化Redis内存的9个要点

    千次阅读 2015-03-23 15:09:19
    优化Redis内存的9个要点
  • Golang优化内存对齐

    千次阅读 2018-12-18 03:57:12
    前文 话说今天在用uintptr进行指针运算的时候,突然想起来有个内存对齐的东西,那么对这个...5.如何利用内存对齐来优化golang? 正文 1.什么是内存对齐? 在想象中内存应该是一个一个独立的字节组成的。像这样:...
  • Android内存优化之图片优化

    万次阅读 多人点赞 2019-04-08 11:52:23
    相信大概刚开始学习Android的时候有过图片过大而直接报错的情况,下面简单介绍一下OOM问题,Android支持的图片格式及图片优化的几种方式 什么是OOM?:Android系统的进程(APP级别)有最大的内存限制,超过这个限制...
  • Android性能优化内存泄漏

    千次阅读 2016-12-11 16:34:40
    内存泄漏(memory leak)是指由于疏忽或错误造成程序未能释放已经不再使用的内存。那么在Android中,当一个对象持有Activity的引用,如果该对象不能被系统回收,那么当这个Activity不再使用时,这个Activity也不会被...
  • java虚拟机内存优化tomat内存配置

    万次阅读 2015-02-26 14:53:23
    java系统运行的效率和内存配置有很大关系 先上一个tomcat 里面设置内存的配置 JAVA_OPTS="-server -Xms1024m -Xmx2048m -Xmn1024m -Xss512K -XX:PermSize=128M -XX:MaxPermSize=256m -XX:NewSize=128m -XX:...
  • 优化Apache内存

    万次阅读 2011-03-31 10:28:00
    Apache内存使用量可以使用下面命令: ps -U apache u|awk '{S+=$6} END {print S}' 优化Apache(httpd) <br /> KeepAlive 是否允许持续连接 MaxKeepAliveRequests 允许的持续连接的最大...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 952,910
精华内容 381,164
关键字:

优化一下内存