1、Main.storyboard 操作不当导致的循环引用
我们首先利用 Xocde 建立一个 Singlle View App 模板的 Project ,然后建一个 MainViewController 类,将它设置为 rootViewController, Demo 目录如下图所示:
以上部分是改动的所有地方的,其余代码都是创建时生成的,没有改动;
我们启动程序,然后检测内存情况,如下图所示:

通过 Intruments 分析,我们发现 APP 启动就开始内存泄露了,然后进一步分析:发现是 window 循环引用了!可是我们没做什么操作啊?难道写个 rootViewController 还会导致内存泄露?
我们进一步分析:

这时,我们发现 Intruments 提到了storyBoard,难道是 storyBoard 导致的 window 循环引用了?还真有可能,立刻联想到 info.plist 文件的配置:

我们看到在 info.plist 文件里配置了 Main.StroryBoard ,我们把这项配置删除,再次运行程序,检测内存:

这时再也没有内存报错了。这证明了我们刚才的猜想,确实是 info.plist 文件里 Main.StroryBoard 的配置导致的 window 循环引用。
所以,如果我们不使用 Main.StroryBoard,记得要去 info.plist 文件里删除相关配置,否则会导致内存泄露!
2、NSURLSession 使用不当导致的内存泄露
注意:在这里我们只讨论通过配置 Configuration 创建 session 导致的循环引用,至于 sharedSession 不在讨论范围;
我们新建一个Demo,给界面绘制一个按钮,绑定一个点击事件:
- (IBAction)getRequestClick:(UIButton *)sender
{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://itunes.apple.com/lookup?id=1164001330"]];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
}];
[dataTask resume];
}
我们点击按钮,发送网络会话,监测内存问题:

可以看到,内存泄露严重,Intruments 指明了导致内存泄露的原因是我们调用了 getRequestClick: 方法,可是在这个方法里我们只是发送了一个简单的网络会话怎么可能内存泄露呢?
我们还是苹果官方文档找下答案吧:

这段话的意思就是:session 是不会主动释放的,除非我们使它无效;否则将会导致内存泄露。
明白了怎么回事,我们再去修改程序:
- (IBAction)getRequestClick:(UIButton *)sender
{
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://itunes.apple.com/lookup?id=1164001330"]];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[session finishTasksAndInvalidate];
}];
[dataTask resume];
}
可以对比到:我们只是在会话结束后,调用了 finishTasksAndInvalidate 方法使 session 无效,这时再检测下内存泄露:

可以看到,这时程序流畅的执行,没有内存泄露了;
建议:如果使用 NSURLSession 发起会话,最好创建一个单例使用;当然,对于特殊的任务,创建了 session 后,记得在收到响应后使 session 无效
3、AFNetworking 使用不当导致的循环泄露
我们新建一个Demo,导入 AFNetworking 库,发送一个 get 请求:
- (IBAction)getRequestClick:(UIButton *)sender
{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager POST:@"http://itunes.apple.com/lookup?id=1164001330" parameters:@{} progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
}];
}
现在我们发送网络会话,监测内存问题:

可以看到,内存泄露严重,Intruments 指明了导致内存泄露的原因是AFHTTPSessionManager,怎么可能呢?这可是一群大神的开源啊!别急,我们进一步去分析:
@interface AFHTTPSessionManager : AFURLSessionManager
@end
@interface AFURLSessionManager : NSObject
@property (readonly, nonatomic, strong) NSURLSession *session;
@end
我们找到 AFHTTPSessionManager 是 AFURLSessionManager 的子类,而 AFURLSessionManager 中持有 NSURLSession,现在原因已经很明显了,很有可能是 NSURLSession 操作不当导致的内存泄露,我们去阅读 AFNetworking 的源码,发现 AFNetworking 确实不曾处理 session ,所以我们需要在网络会话结束后处理 Session :
- (IBAction)getRequestClick:(UIButton *)sender
{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager POST:@"http://itunes.apple.com/lookup?id=1164001330" parameters:@{} progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
[manager.session finishTasksAndInvalidate];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
[manager.session finishTasksAndInvalidate];
}];
}
我们在网络会话结束(不论成功或者失败)后,使 session 无效,这时再去监测内存,发现已经没有内存泄露了。
建议:如果使用 AFNetworking 发起会话,最好创建一个单例使用;当然,对于特殊的任务,创建了 manager 后,记得在收到响应后使 session 无效
4、使用 Core Foundation 疏忽造成的内存泄露
我在程序的启动方法里发现这样一段代码:
ABAddressBookRef addresBook = ABAddressBookCreateWithOptions(NULL, NULL);
ABAddressBookRegisterExternalChangeCallback(addresBook, addressBookChanged, (__bridge void *)(self));
程序启动后,发现内存泄露:

泄露的原因是 ABAddressBookRef 导致的;我们知道是因为没有 CFRelease 的原因。
这里,我们再次强调:ARC 模式的内存管理只会对 OC 对象进行管理;苹果有句名言:ARC is only for NSObject。但是对 C 对象或是 CG 开头的对象,即存在于 Core Foundation框架 (CoreFoundation.framework 是一组C语言接口)中的对象无效,需要我们手动的 retain 和 release。
所以,即使我们使用 ARC 来管理内存,也要细心于 Core Foundation 对象。
5、使用 HBRSAHandler 工具库进行 RSA 加密导致的内存泄露
我们的项目使用 HBRSAHandler 工具对请求数据进行加密,使用 Intruments 对内存监控,发现每发起一个网络会话,就错造成一次内存泄露:

泄露的关键代码如下:
HBRSAHandler* handler = [HBRSAHandler new];
[handler importKeyWithType:KeyTypePrivate andkeyString:private_key_string];
[handler importKeyWithType:KeyTypePublic andkeyString:public_key_string];
NSString* sig = [handler signString:@"Hello Word"];
NSString* sigMd5 = [handler signMD5String:@"Hello Word"];
可以看到,我们的调用没有问题啊,估计是这个工具封装的有问题:
我们来看下内部实现方法:
- (NSString *)signMD5String:(NSString *)string
{
if (!_rsa_pri) {
NSLog(@"please import private key first");
return nil;
}
const char *message = [string cStringUsingEncoding:NSUTF8StringEncoding];
//int messageLength = (int)strlen(message);
unsigned char *sig = (unsigned char *)malloc(256);
unsigned int sig_len;
unsigned char digest[MD5_DIGEST_LENGTH];
MD5_CTX ctx;
MD5_Init(&ctx);
MD5_Update(&ctx, message, strlen(message));
MD5_Final(digest, &ctx);
int rsa_sign_valid = RSA_sign(NID_md5
, digest, MD5_DIGEST_LENGTH
, sig, &sig_len
, _rsa_pri);
if (rsa_sign_valid == 1) {
NSData* data = [NSData dataWithBytes:sig length:sig_len];
NSString * base64String = [data base64EncodedStringWithOptions:0];
free(sig);
return base64String;
}
free(sig);
return nil;
}
我发现这个方法内部有个全局变量:_rsa_pri ,它是 RSA 类型(不是一个OC 类),我猜测多半是由于 _rsa_pri 引起的内存泄露,然后我在整个 HBRSAHandler 类里搜索 _rsa_pri,确实没有发现 free(_rsa_pri) 这句代码,这证实了我的猜想,既然已经找到了问题,那么我们就可以给出解决思路: _rsa_pri 变量保持全局唯一。这是有可能实现的,因为我们的 app 一般只有一对公私匙,我们可以创建一个 HBRSAHandler 单例,使 _rsa_pri 变量唯一,不多次去创建它(当然,我们可以去这个工具库修改源代码,但我不建议你这样做)
由于使用全局单例,而这个加密方法不是线程安全的,所以我们在加密时一定要使用锁确保线程安全。
注意:监测内存泄露时,一定要关闭 调试僵尸对象 的选项,否则 Intruments 会监测到大量 NSZombie 对象;因为调试僵尸对象的原理是,在对象释放(retainCount为0)时,使用一个内置的Zombie对象,替代原来被释放的对象;无论向该对象发送什么消息(函数调用),都会触发异常,抛出调试信息。也就是说,使用 Intruments 监测到了大量没有释放掉内存的 Zombie 对象,这显然不合情理,所以记得关闭 调试僵尸对象。
......................... 未完,持续更新中 ........................
网友评论