乐趣区

关于android:百度工程师移动开发避坑指南内存泄漏篇

作者 | 启明星小组

在日常编写代码时难免会遇到各种各样的问题和坑,这些问题可能会影响咱们的开发效率和代码品质,因而咱们须要一直总结和学习,以防止这些问题的呈现。接下来咱们将围绕挪动开发中常见问题做出总结,以进步大家的开发品质。本系列文章讲围绕内存透露、语言开发注意事项等开展。本篇咱们将介绍 Android/iOS 常见的内存透露问题。

一、Android 端

内存透露(Memory Leak),简略说就是不再应用的对象无奈被 GC 回收,占用内存无奈开释,导致利用占用内存越来越多,内存空间有余而呈现 OOM 解体;另外因为内存可用空间变少,GC 更加频繁,更容易触发 FULL GC,进行线程工作,导致利用卡顿。

Android 应用程序中的内存透露是一种常见的问题,以下是一些常见的 Android 内存透露:

1.1 匿名外部类

匿名外部类持有外部类的援用,匿名外部类对象泄露,从而导致外部类对象内存透露,常见 Handler、Runnable 匿名外部类,持有内部 Activity 的援用,如果 Activity 曾经被销毁,然而 Handler 未解决完音讯,导致 Handler 内存泄露,从而导致 Activity 内存泄露。

示例 1:

public class TestActivity extends AppCompatActivity {

    private static final int FINISH_CODE = 1;

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {if (msg.what == FINISH_CODE) {TestActivity.this.finish();
            }
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);
        handler.sendEmptyMessageDelayed(FINISH_CODE, 60000);
    }
}

示例 2:

public class TestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {TestActivity.this.finish();
            }
        }, 60000);
    }
}

示例 1 和示例 2 均为简略计时一分钟敞开页面,如果页面在之前被被动敞开销毁,Handler 中仍有音讯期待执行,就存在到 Activity 的援用链,导致 Activity 销毁后无奈被 GC 回收,造成内存泄露;示例 1 为 Handler 匿名外部类,持有内部 Activity 援用:主线程 —> ThreadLocal —> Looper —> MessageQueue —> Message —> Handler —> Activity;示例 2 为 Runnable 匿名外部类,持有内部 Activity 援用:Message —> Runnable —> Activity.

修复办法 1: 次要针对 Handler,在 Activity 生命周期移除所有音讯。

    @Override
    protected void onDestroy() {super.onDestroy();
        handler.removeCallbacksAndMessages(null);
    }

修复办法 2: 动态外部类 + 弱援用,去掉强援用关系,能够修复相似匿名外部类造成内存泄露。

    static class FinishRunnable implements Runnable {

        private WeakReference<Activity> activityWeakReference;

        FinishRunnable(Activity activity) {activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void run() {Activity activity = activityWeakReference.get();
            if (activity != null) {activity.finish();
            }
        }
    }
    
    new Handler().postDelayed(new FinishRunnable(TestActivity.this), 60000);

1.2 单例 / 动态变量

单例 / 动态变量持有 Activity 的援用,即便 Activity 曾经被销毁,它的援用依然存在,从而导致内存透露。

示例:

    static class Singleton {

        private static Singleton instance;

        private Context context;

        private Singleton(Context context) {this.context = context;}

        public static Singleton getInstance(Context context) {if (instance == null) {instance = new Singleton(context);
            }
            return instance;
        }
    }
    
    Singleton.getInstance(TestActivity.this);

调用示例中的单例,传递 Context 参数,应用 Activity 对象,即便 Activity 销毁,也始终被动态变量 Singleton 援用,导致无奈回收造成内存泄露。

修复办法:

Singleton.getInstance(Application.this);

尽量应用 Application 的 Context 作为单例参数,除非一些须要须要 Activity 的性能,比方显示 Dialog,如果非要应用 Activity 作为单例参数,能够参考匿名外部类修复办法,在适合机会比方 Activity 的 onDestroy 生命周期开释单例,或者应用弱援用持有 Activity。

1.3 监听器

示例: EventBus 注册监听未解绑,导致注册到 EventBus 始终被援用,无奈回收。

public class TestActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);
        EventBus.getDefault().register(this);
    }
}

修复办法: 在对应注册监听的生命周期解绑,onCreate 对应 onDestroy。

    @Override
    protected void onDestroy() {super.onDestroy();
        EventBus.getDefault().unregister(this);
    }

1.4 文件 / 数据库资源

示例 : 关上文件数据库或者文件,产生异样,未敞开,导致资源始终存在,导致内存透露。

    public static void copyStream(File inFile, File outFile) {
        try {FileInputStream inputStream = new FileInputStream(inFile);
            FileOutputStream outputStream = new FileOutputStream(outFile);
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {outputStream.write(buffer, 0, len);
            }
        } catch (IOException e) {e.printStackTrace();
        }
    }

修复 :在 finally 代码块中敞开文件流,保障产生异样后肯定能执行到

    public static void copyStream(File inFile, File outFile) {
        FileInputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {inputStream = new FileInputStream(inFile);
            outputStream = new FileOutputStream(outFile);
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {outputStream.write(buffer, 0, len);
            }
        } catch (IOException e) {e.printStackTrace();
        } finally {close(inputStream);
            close(outputStream);
        }
    }

    public static void close(Closeable closeable) {if (closeable != null) {
            try {closeable.close();
            } catch (Exception e) {e.printStackTrace();
            }
        }
    }

1.5 动画

示例: Android 动画未及时勾销开释动画资源,导致内存泄露。

public class TestActivity extends AppCompatActivity {

    private ImageView imageView;
    private Animation animation;

    @Override
    protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        imageView = (ImageView) findViewById(R.id.image_view);
        animation = AnimationUtils.loadAnimation(this, R.anim.test_animation);
        imageView.startAnimation(animation);
    }
}

修复 : 在页面退出销毁时勾销动画,及时开释动画资源。

@Override
protected void onDestroy() {super.onDestroy();
    if (animation != null) {animation.cancel();
        animation = null;
    }
}

二、IOS 端

目前咱们曾经有了 ARC(主动援用计数)来代替 MRC(手动援用计数),申请的对象在没有被强援用时会主动开释。但在编码不标准的状况下,援用计数无奈及时归零,还是会存在引入内存泄露的危险,这可能会造成一些十分重大的结果。以直播场景举例,如果直播业务的 ViewController 无奈开释,会导致依赖于 ViewController 的点位统计数据异样,且用户敞开直播页面后依然能够听到直播声音。相熟内存透露场景、养成防止内存泄露的习惯是非常重要的。上面介绍一些 iOS 常见内存透露及解决方案。

2.1 block 引起的循环援用

block 引入的循环援用是最常见的一类内存泄露问题。常见的援用环是对象 ->block-> 对象,此时对象和 block 的援用计数均为 1,无奈被开释。

[self.contentView setActionBlock:^{[self doSomething];
}];

例子代码中,self 强援用成员变量 contentView,contentView 强援用 actionBlock,actionBlock 又强援用了 self,引入内存泄露问题。

解除循环援用,就是解除强援用环,须要将某一强援用替换为弱援用。如:

__weak typeof(self) weakSelf = self;
[self.contentView setActionBlock:^{__strong typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf doSomething];
}];

此时 actionBlock 弱援用 self,循环援用被突破,能够失常开释。

或者应用 RAC 提供的更简便的写法:

@weakify(self);
[self setTaskActionBlock:^{@strongify(self);
    [self doSomething];
}];

须要留神的是,可能和 block 存在循环援用的不仅仅是 self,所有实例对象都有可能存在这样的问题,而这也是开发过程中很容易疏忽的。比方:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {Cell *cell = [tableView dequeueReusableCellWithIdentifier:@"identifer"];
    @weakify(self);
    cell.clickItemBlock = ^(CellModel * _Nonnull model) {@strongify(self);
        [self didSelectRowMehod:model tableView:tableView];
    };
    return cell;
}

这个例子中,self 和 block 之间的循环援用被突破,self 能够失常开释了,然而须要留神的是还存在一条循环援用链:tableView 强援用 cell,cell 强援用 block,block 强援用 tableView。这同样会导致 tableView 和 cell 无奈开释。

正确的写法为:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {Cell *cell = [tableView dequeueReusableCellWithIdentifier:@"identifer"];
    @weakify(self);
    @weakify(tableView);
    cell.clickItemBlock = ^(CellModel * _Nonnull model) {@strongify(self);
        @strongify(tableView);
        [self didSelectRowMehod:model tableView:tableView];
    };
    return cell;
}

2.2 delegate 引起的循环援用

@protocol TestSubClassDelegate <NSObject>

- (void)doSomething;

@end

@interface TestSubClass : NSObject

@property (nonatomic, strong) id<TestSubClassDelegate> delegate;

@end

@interface TestClass : NSObject <TestSubClassDelegate>

@property (nonatomic, strong) TestSubClass *subObj;

@end

上述例子中,TestSubClass 对 delegate 应用了 strong 修饰符,导致设置代理后,TestClass 实例和 TestSubClass 实例互相强援用,造成循环援用。大部分状况下,delegate 都须要应用 weak 修饰符来防止循环援用。

2.3 NSTimer 强援用

self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
[NSRunLoop.currentRunLoop addTimer:self.timer forMode:NSRunLoopCommonModes];

NSTimer 实例会强援用传入的 target,就会呈现 self 和 timer 的互相强援用。此时必须手动保护 timer 的状态,在 timer 进行或 view 被移除时,被动销毁 timer,突破循环援用。

解决方案 1 :换用 iOS10 后提供的 block 形式,防止 NSTimer 强援用 target。

@weakify(self);
self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {@strongify(self);
    [self doSomething];
}];

解决方案 2 :应用 NSProxy 解决强援用问题。

// WeakProxy
@interface TestWeakProxy : NSProxy

@property (nullable, nonatomic, weak, readonly) id target;

- (instancetype)initWithTarget:(id)target;

+ (instancetype)proxyWithTarget:(id)target;

@end

@implementation TestWeakProxy

- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}

+ (instancetype)proxyWithTarget:(id)target {return [[TestWeakProxy alloc] initWithTarget:target];
}

- (void)forwardInvocation:(NSInvocation *)invocation {if ([self.target respondsToSelector:[invocation selector]]) {[invocation invokeWithTarget:self.target];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {return [self.target methodSignatureForSelector:aSelector];
}

- (BOOL)respondsToSelector:(SEL)aSelector {return [self.target respondsToSelector:aSelector];
}

@end

// 调用
self.timer = [NSTimer timerWithTimeInterval:1 target:[TestWeakProxy proxyWithTarget:self] selector:@selector(doSomething) userInfo:nil repeats:YES];

2.4 非援用类型内存透露

ARC 的主动开释是基于援用计数来实现的,只会保护 oc 对象。间接应用 C 语言 malloc 申请的内存,是不被 ARC 治理的,须要手动开释。常见的如应用 CoreFoundation、CoreGraphics 框架自定义绘图、读取文件等操作。

如通过 CVPixelBufferRef 生成 UIImage:

CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CIImage* bufferImage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef frameCGImage = [context createCGImage:bufferImage fromRect:bufferImage.extent];
UIImage *uiImage = [UIImage imageWithCGImage:frameCGImage];
CGImageRelease(frameCGImage);
CFRelease(sampleBuffer);

2.5 提早开释问题

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(20 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{[self doSomething];
});

上述例子中,应用 dispatch\_after 提早 20 秒后执行 doSomething 办法。这并不会造成 self 对象的内存透露问题。但假如 self 是一个 UIViewController,即便 self 曾经从导航栈中移除,不须要再应用了,self 也会在 block 执行后才会被开释,造成业务上呈现相似内存泄露的景象。

在这种长时间的延时执行中,最好也退出 weakify-strongify 对,防止强持有。

@weakify(self);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(20 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{@strongify(self);
    [self doSomething];
});

———-  END  ———-

举荐浏览【技术加油站】系列:

百度程序员开发避坑指南(Go 语言篇)

百度程序员开发避坑指南(3)

百度程序员开发避坑指南(挪动端篇)

百度程序员开发避坑指南(前端篇)

退出移动版