Android开发笔记——常见BUG类型之内存泄露与线程安全

本文内容来源于最近一次内部分享的总结,没来得及详细整理,见谅。

本次分享主要对内存泄露线程安全这两个问题进行一些说明,内部代码扫描发现的BUG大致分为四类:1)空指针;2)除0;3)内存、资源泄露;4)线程安全。第一、二个问题属于编码考虑不周,第三、四个问题则需要更深入的分析。

  1. 内存泄露

  2. 线程安全

一、内存泄露

1、很抱歉,”XXX”已停止运行。OOM?

怎样才能让app报OOM呢?最简单的办法如下:

Bitmap bt1 = BitmapFactory.decodeResource(this.getResources(), R.drawable.image);
Bitmap bt2 = BitmapFactory.decodeResource(this.getResources(), R.drawable.image);
Bitmap btn = ...

2、查看内存占用

  • 命令行:adb shell dumpsys meminfo packageName

  • 通过Android Studio的Memory Monitor查看内存中Dalvik Heap的实时变化

3、发生内存泄露的条件

首先,每个app有最大内存限制。

ActivityManager activityManager = (ActivityManager) context.getSystemServiceContext.ACTIVITY_SERVICE);
activityManager.getMemoryClass();
 
getMemoryClass()取到的是最大内存资源。Android中的堆内存分为Native Heap和Dalvik Heap。C/C++申请的内存空间在Native Heap中,Java申请的内存空间则在Dalvik Heap中。对于head堆的大小限制,可以查看/system/build.prop文件:
 

dalvik.vm.heapstartsize=8m
dalvik.vm.heapgrowthlimit=96m
dalvik.vm.heapsize=256m

注意: 

heapsize参数表示单个进程heap可用的最大内存,但如果存在以下参数”dalvik.vm.headgrowthlimit =96m”表示单个进程heap内存被限定在96m,即程序运行过程实际只能使用96m内存。

如果申请的内存资源超过上述限制,系统就会抛出OOM错误。

4、常见避免OOM的措施

以下主要从四个方面总结常见的措施:1)减小对象的内存占用;2)内存对象的重复利用;3)避免对象的内存泄露;4)内存使用策略优化。

4.1 减小对象的内存占用

4.2 内存对象的重复利用

  • ListView/GridView等出现大量重复子组件的视图里面对ConvertView的复用

  • 使用LRU机制缓存Bitmap
  • 避免在onDraw方法里面执行对象的创建
  • 使用StringBuilder来替代频繁的”+”

4.3 避免对象的内存泄露

4.1和4.2都是比较常规的措施,4.3需要重点关注。

1)Activity泄露

导致Activity泄露的原因较多,下面列举一些比较常见的。从原理上主要分为两类:i)静态对象;ii)this$0

  • Activity被static变量引用。这段代码来自于我们的Crash上传

    private static Map<ComponentName, ExceptionHandler> configMap =
                            new HashMap<ComponentName, ExceptionHandler>();
    public static void setActivity(final Activity activity, boolean send2Server) {
        Log.d(TAG, "bind exception handler : " + activity.getComponentName().getClassName());
        //上下文初始化
        SDKContext.init(activity.getApplication());
        init(activity.getApplication());
        ExceptionHandler exceptionHandler = new ExceptionHandler(
                            activity, send2Server, Thread.getDefaultUncaughtExceptionHandler());
        configMap.put(activity.getComponentName(), exceptionHandler);
        Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
    }

下面是通过MAT分析一个Activity泄露的截图:
 

  • 内部类引用导致Activity的泄漏
    最典型的场景是Handler导致的Activity泄漏,如果Handler中有延迟的任务或者是等待执行的任务队列过长,都有可能因为Handler继续执行而导致Activity发生泄漏。此时的引用关系链是Looper -> MessageQueue -> Message -> Handler -> Activity。为了解决这个问题,可以在UI退出之前,执行remove Handler消息队列中的消息与runnable对象。或者是使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的。
    可参考链接:线程通信

2)考虑使用Application Context而不是Activity Context

对于大部分非必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),我们都可以考虑使用Application Context而不是Activity的Context,这样可以避免不经意的Activity泄露。

3)注意临时Bitmap对象的及时回收

虽然在大多数情况下,我们会对Bitmap增加缓存机制,但是在某些时候,部分Bitmap是需要及时回收的。例如临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原始bitmap所占用的空间。

4)内存占用监控
通过Runtime获取maxMemory,而maxMemory-totalMemory即为剩余可使用的dalvik内存。定期检查这个值,达到80%就去释放各种cache资源(bitmap的cache)。


/**
 * Returns the maximum number of bytes the heap can expand to. See {@link #totalMemory} for the
 * current number of bytes taken by the heap, and {@link #freeMemory} for the current number of
 * those bytes actually used by live objects.
 */
int maxMemory = Runtime.getRuntime().maxMemory()); // 应用程序最大可用内存
/**
 * Returns the number of bytes taken by the heap at its current size. The heap may expand or
 * contract over time, as the number of live objects increases or decreases. See
 * {@link #maxMemory} for the maximum heap size, and {@link #freeMemory} for an idea of how much
 * the heap could currently contract.
 */
long totalMemory = Runtime.getRuntime().totalMemory()); // 应用程序已获得内存
/**
 * Returns the number of bytes currently available on the heap without expanding the heap. See
 * {@link #totalMemory} for the heap‘s current size. When these bytes are exhausted, the heap
 * may expand. See {@link #maxMemory} for that limit.
 */
long freeMemory = Runtime.getRuntime().freeMemory()); // 应用程序已获得内存中未使用内存

5)注意Cursor对象是否及时关闭

在程序中我们经常会进行查询数据库的操作,但时常会存在不小心使用Cursor之后没有及时关闭的情况。这些Cursor的泄露,反复多次出现的话会对内存管理产生很大的负面影响,我们需要谨记对Cursor对象的及时关闭。

4.4 内存使用策略优化

  • 谨慎使用large heap

  • 综合考虑设备内存阈值与其他因素设计合适的缓存大小
  • onLowMemory()/onTrimMemory(int)
  • 资源文件需要选择合适的文件夹进行存放
  • Try catch某些大内存分配的操作
  • 谨慎使用static对象
  • 优化布局层次,减少内存消耗
  • 谨慎使用多进程
  • 谨慎使用依赖注入框架
  • 使用ProGuard来剔除不需要的代码
  • 谨慎使用第三方libraries
  • 考虑不同的实现方式来优化内存占用

二、线程安全

1、下面的方法是线程安全的吗?


class MyCounter {
    private static int counter = 0;
    public static int getCount() {
        return counter++;
    }
}

怎样使上述方法线程安全?

 

2、Java中的线程安全

怎样保持在多线程环境下的数据一致性,Java提供了多种方法实现:

  1. synchronized

  2. java.util.concurrent.atomic
  3. java.util.concurrent.locks
  4. thread safe collection(ConcurrentHashMap)
  5. volatile

2.1 synchronized

JVM保证被synchronized关键字修饰的代码段在同一时间只能被一个线程访问,内部通过对对象加锁来实现的。当方法被synchronized修饰时,锁加在对象上;当方法同时为static时,锁加在类上。从性能的角度来讲,一般不建议直接将锁加在类上,这样会使得类的所有对象的该方法均为synchronized的。

从之前扫描的问题来看,在编写synchronized程序时主要有两点需要注意:

  • synchronized需要创建基于对象或者类的锁,所以不能在构造器或者变量上加锁。

  • synchronized造成死锁。

1) 锁加在哪里?

List<ResultPoint> currentPossible = possibleResultPoints;
List<ResultPoint> currentLast = lastPossibleResultPoints;
int frameLeft = frame.left;
int frameTop = frame.top;
if (currentPossible.isEmpty()) {
    lastPossibleResultPoints = null;
} else {
    possibleResultPoints = new ArrayList<>(5);
    lastPossibleResultPoints = currentPossible;
    paint.setAlpha(CURRENT_POINT_OPACITY);
    paint.setColor(resultPointColor);
    synchronized (currentPossible) {
        for (ResultPoint point : currentPossible) {
            canvas.drawCircle(frameLeft
                    + (int) (point.getX() * scaleX), frameTop
                    + (int) (point.getY() * scaleY), POINT_SIZE,
                    paint);
        }
    }
}

上述方法中,possibleResultPoints的创建没有采用同步措施,需要使用Collections.synchronizedXxx


List<MyType> list = Collections.synchronizedList(new ArrayList(<MyType>));
...
synchronized(list){
    for(MyType m : list){
        foo(m);
        m.doSomething();
    }
}

一般比较推荐创建一个虚拟的对象专门用于获取锁。

//dummy object variable for synchronization
private Object mutex=new Object();
...
//using synchronized block to read, increment and update count value synchronously
synchronized (mutex) {
        count++;
}

PS:直接在方法上加synchronized可能DoS攻击喔,举个栗子:

public class MyObject {
    // Locks on the object‘s monitor
    public synchronized void doSomething() {
    // ...
    }
}
// 黑客的代码
MyObject myObject = new MyObject();
synchronized (myObject) {
    while (true) {
        // Indefinitely delay myObject
        Thread.sleep(Integer.MAX_VALUE);
    }
}

黑客的代码获取了MyObject对象的锁,导致doSomething死锁,从而引发Denial of Service。

public class MyObject {
    //locks on the class object‘s monitor
    public static synchronized void doSomething() {
    // ...
    }
}
// 黑客的代码
synchronized (MyObject.class) {
    while (true) {
        Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
    }
}

2) 死锁。

public class ThreadDeadlock {
    public static void main(String[] args) throws InterruptedException {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Object obj3 = new Object();
        Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
        Thread t2 = new Thread(new SyncThread(obj2, obj3), "t2");
        Thread t3 = new Thread(new SyncThread(obj3, obj1), "t3");
        t1.start();
        Thread.sleep(5000);
        t2.start();
        Thread.sleep(5000);
        t3.start();
    }
}
class SyncThread implements Runnable{
    private Object obj1;
    private Object obj2;
    public SyncThread(Object o1, Object o2){
        this.obj1=o1;
        this.obj2=o2;
    }
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + " acquiring lock on "+obj1);
        synchronized (obj1) {
         System.out.println(name + " acquired lock on "+obj1);
         work();
         System.out.println(name + " acquiring lock on "+obj2);
         synchronized (obj2) {
            System.out.println(name + " acquired lock on "+obj2);
            work();
        }
         System.out.println(name + " released lock on "+obj2);
        }
        System.out.println(name + " released lock on "+obj1);
        System.out.println(name + " finished execution.");
    }
    private void work() {
        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上述代码会输出什么呢?

时间: 2024-10-18 19:35:17

Android开发笔记——常见BUG类型之内存泄露与线程安全的相关文章

转:Android开发:使用DDMS Heap进行内存泄露调试

无论怎么小心,想完全避免bad code是不可能的,此时就需要一些工具来帮助我们检查代码中是否存在会造成内存泄漏的地方.Android tools中的DDMS就带有一个很不错的内存监测工具Heap,本文讲解了如何使用Heap来进行内存监测. 内存监测工具 DDMS --> Heap 这里我使用eclipse的ADT插件,并以真机为例,在模拟器中的情况类似.用 Heap监测应用进程使用内存情况的步骤如下: 1. 启动eclipse后,切换到DDMS透视图,并确认Devices视图.Heap视图都是

【转】Android开发笔记(序)写在前面的目录

原文:http://blog.csdn.net/aqi00/article/details/50012511 知识点分类 一方面写写自己走过的弯路掉进去的坑,避免以后再犯:另一方面希望通过分享自己的经验教训,与网友互相切磋,从而去芜存菁进一步提升自己的水平.因此博主就想,入门的东西咱就不写了,人不能老停留在入门上:其次是想拾缺补漏,写写虽然小众却又用得着的东西:另外就是想以实用为主,不求大而全,但求小而精:还有就是有的知识点是java的,只是Android开发也会经常遇上,所以蛮记下来.个人的经

Android 性能优化之使用MAT分析内存泄露问题

我们平常在开发Android应用程序的时候,稍有不慎就有可能产生OOM,虽然JAVA有垃圾回收机,但也不能杜绝内存泄露,内存溢出等问题,随着科技的进步,移动设备的内存也越来越大了,但由于Android设备的参差不齐,可能运行在这台设备好好的,运行在那台设备就报OOM,这些适配问题也是比较蛋疼的,比如我们平常运行着一个应用程序,运行的好好的,突然到某个Activity就给你爆出一个OOM的错误,你可能会以为是这个Activity导致的内存泄露,你会想到也有可能是内存有泄露吗?内存泄露就像一个定时炸

Android开发笔记(一百一十)使用http框架上传文件

HTTP上传 与文件下载相比,文件上传的场合不是很多,通常用于上传用户头像.朋友圈发布图片/视频动态等等,而且上传文件需要服务器配合,所以容易被app开发者忽略.就上传的形式来说,app一般采用http上传文件,很少用ftp上传文件. HttpURLConnection上传 很可惜Android没有提供专门的文件上传工具类,所以我们要自己写代码实现上传功能了.其实也不难,一样是按照普通网络访问的POST流程,只是要采用"multipart/form-data"方式来分段传输.另外文件上

Android开发笔记

一.Android的体系结构 1.1 体系结构 从下往上进行层概述: Linux内核层: 该层主要的作用是:对系统进行管理例如:驱动管理,电源管理,进程管理,安全管理,内存管理等等. 硬件抽象层: 该层主要的作用是:对Linux底层实现进行屏蔽,向上提供接口,该层不开源,主要是为了维护商家的利益,但是会影响系统的性能 Android对硬件的支持分为两层:一层是用户空间,另一层是内核空间, 用户空间存放的是:硬件抽象层,该层提供具体的访问细节 内核空间存放的是:Linux驱动程序,该层只是提供简单

android开发笔记之自定义开关按钮

今天来讲讲自定义单个控件,就拿开关按钮来讲讲,相信大家见了非常多这样的了,先看看效果: 我们可以看到一个很常见的开关按钮,那就来分析分析. 首先: 这是由两张图片构成: ①一张为有开和关的背景图片 ②一张为控制开和关的滑动按钮 第一步: 写个类继承View,并重写几个方法: 第一个为构造函数,重写一个参数的函数和两个参数的函数就够了,因为两个参数的函数能够使用自定义属性 第二个为控制控件的大小–>protected void onMeasure(int widthMeasureSpec, int

Android开发笔记(一百零八)智能语音

智能语音技术 如今越来越多的app用到了语音播报功能,例如地图导航.天气预报.文字阅读.口语训练等等.语音技术主要分两块,一块是语音转文字,即语音识别:另一块是文字转语音,即语音合成. 对中文来说,和语音播报相关的一个技术是汉字转拼音,想想看,拼音本身就是音节拼读的标记,每个音节对应一段音频,那么一句的拼音便能用一连串的音频流合成而来.汉字转拼音的说明参见<Android开发笔记(八十三)多语言支持>. 语音合成通常也简称为TTS,即TextToSpeech(从文本到语言).语音合成技术把文字

[置顶] Android开发笔记(成长轨迹)

分类: 开发学习笔记2013-06-21 09:44 26043人阅读 评论(5) 收藏 Android开发笔记 1.控制台输出:called unimplemented OpenGL ES API 调用了未实现的OpenGL ES API函数,一般由于导入的第三方库如地图库,里面有用到OpenGL,但是模拟器的硬件默认是没有这个的,所以需要我们编辑模拟器Emulation Options选项勾选 Use Host GPU 然后重启模拟器再尝试,如果还是这个错误,那么我们只好用真机测试了. 2.

Android开发笔记(一百一十七)app省电方略

电源管理PowerManager PowerManager是Android的电源管理类,用于管理电源操作如睡眠.唤醒.重启以及调节屏幕亮度等等. PowerManager的对象从系统服务POWER_SERVICE中获取,它的主要方法如下: goToSleep : 睡眠,即锁屏. wakeUp : 唤醒,即解锁. reboot : 重启. 另有下列几个隐藏的方法: getMinimumScreenBrightnessSetting : 获取屏幕亮度的最小值. getMaximumScreenBri