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


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

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

    1. 内存泄露
    2. 线程安全

    一、内存泄露

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

    20160509-1

    怎样才能让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

    20160509-2

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

    20160507-2

     

    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泄露的截图:
     20160512-1

    • 内部类引用导致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();
            }
        }
    }

    上述代码会输出什么呢?

  • 相关阅读:
    cmd输出的日志里有中文乱码的解决办法
    自定义控件ToggleButton滑动开关
    移除指定位置的jsonarray
    设置Listview不滚动
    Volley框架学习
    LoaderManager的使用
    Activity获取Fragment的值
    Fragment和Fragment进行数据传递
    Fragmet的学习
    android ListView上拉加载更多
  • 原文地址:https://www.cnblogs.com/younghao/p/5498763.html
Copyright © 2020-2023  润新知