iOS -- 关于内存管理的理解
因为大多数情况都是自己独立开发,所以平常都不会刻意的去检测内存。只有在app发生bug的时候,才去查是不是内存泄露引起的bug问题。这种做法肯定是不可取的,能把问题解决在萌芽,还是要尽早解决。
如何检测内存是否泄露
检查定位内存泄露的方法这里介绍两种.
1.使用Xcode检测
在我们运行项目的时候,xcode会显示当前运行项目的内存大小.
截屏2022-06-24 14.54.08.png
点击Memory 就能看到一个内存使用情况,如果我们要定位内存泄露的代码,需要点击profile in instruments 按钮,这时候就进入到了leaks界面了然后再如下设置
截屏2022-06-20 14.38.01.png
双击代码行就可以定位到xcode内存泄露的代码行了.
2 使用MLeaksFinder
使用xcode leaks更适合大范围的查找,全部的泄漏点.而MLeaksFinder在我们开发过程中,有泄露就会提示出泄露的地方.
在开发过程中,离开当前界面的时候是需要释放掉的.我们可以使用dealloc方法来查看,当离开界面的时候dealloc没有调用,那说明当前的界面没有释放
但是我们定位不到具体是哪出现了内存泄露.使用MLeaksFinder可以帮助定位.
原理:为基类NSObject添加一个方法 willdealoc方法.然后用weak弱指针指向self,在2s后,通过这个弱指针区调用assertNotDealloc.
- (BOOL)willDealloc {
__weak id weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf assertNotDealloc];
});
return YES;
}
- (void)assertNotDealloc {
NSAssert(NO, @“”);
}
如果当前界面没有释放,那么self就存在,就回去调用assertNotDealloc方法.这个时候就需要报告泄露的地址了
- (void)assertNotDealloc {
// 是否已经添加进泄漏对象名单
if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
return;
}
// 添加进内存泄露名单
[MLeakedObjectProxy addLeakedObject:self];
NSString *className = NSStringFromClass([self class]);
NSLog(@"Possibly Memory Leak.\nIn case that %@ should not be dealloced, override -willDealloc in %@ by returning NO.\nView-ViewController stack: %@", className, className, [self viewStack]);
}
- (NSSet *)parentPtrs {
NSSet *parentPtrs = objc_getAssociatedObject(self, kParentPtrsKey);
if (!parentPtrs) {
parentPtrs = [[NSSet alloc] initWithObjects:@((uintptr_t)self), nil];
}
return parentPtrs;
}
通过构造了一个MLeakedObjectProxy对象,并将其加入到leakedObjectPtrs集合中,弹出alert提示框.然后找到引用环.这样就可以具体定位到泄露的地址了.
引起内存泄露的总结
-1. NStime内存泄露
self.testtimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runbtnclick) userInfo:nil repeats:YES];
-(void)runbtnclick{
NSLog(@"定时器");
}
-(void)dealloc{
NSLog(@"销毁当前界面");
}
运行上面的代码,能发现并没执行dealloc里面的打印.说明当前界面并没有释放. 造成这个的原因是:定时器还一直在走,打印"定时器".当前界面self持有timer,而在timer里面target 又持有了self,所以造成了循环引用,释放不掉.
那应该如何解决呢?
- (1) 在退出当前面时,把它至为空
-(void)viewWillDisappear:(BOOL)animated{
[self.testtimer invalidate];
self.testtimer = nil;
}
- (2)可以使用block来实现
self.testtimer=[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
// NSLog(@"定时器");
// }];
这样就不会相互持有.
- 2 wkwebview和js交互
项目中,wkwebview和H5需要交互.这个时候就会用到
[configuration.userContentController addScriptMessageHandler:self name:@"openUserAgreement"];
#pragma mark - WKScriptMessageHandler
/*
1、js调用原生的方法就会走这个方法
2、message参数里面有2个参数我们比较有用,name和body,
2.1 :其中name就是之前已经通过addScriptMessageHandler:name:方法注入的js名称
2.2 :其中body就是我们传递的参数了,我在js端传入的是一个字典,所以取出来也是字典,字典里面包含原生方法名以及被点击图片的url
*/
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
//NSLog(@"%@,%@",message.name,message.body);
NSDictionary *imageDict = message.body;
NSString *src = [NSString string];
if (imageDict[@"imageSrc"]) {
src = imageDict[@"imageSrc"];
}else{
src = imageDict[@"videoSrc"];
}
NSString *name = imageDict[@"methodName"];
//如果方法名是我们需要的,那么说明是时候调用原生对应的方法了
if ([picMethodName isEqualToString:name]) {
SEL sel = NSSelectorFromString(picMethodName);
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
//写在这个中间的代码,都不会被编译器提示PerformSelector may cause a leak because its selector is unknown类型的警告
[self performSelector:sel withObject:src];
#pragma clang diagnostic pop
}else if ([videoMethodName isEqualToString:name]){
SEL sel = NSSelectorFromString(name);
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
[self performSelector:sel withObject:src];
#pragma clang diagnostic pop
}
}
#pragma mark - WKUIDelegate(js弹框需要实现的代理方法)
//使用了WKWebView后,在JS端调用alert()是不会在HTML中显式弹出窗口,是我们需要在该方法中手动弹出iOS系统的alert的
//该方法中的message参数就是我们JS代码中alert函数里面的参数内容
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
// NSLog(@"js弹框了");
UIAlertController *alertView = [UIAlertController alertControllerWithTitle:@"JS-Coder" message:message preferredStyle:UIAlertControllerStyleAlert];
[alertView addAction:[UIAlertAction actionWithTitle:@"Sure" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
//一定要调用下这个block
//API说明:The completion handler to call after the alert panel has been dismissed
completionHandler();
}]];
[self presentViewController:alertView animated:YES completion:nil];
}
在上面的代码中是存在相互引用的内存泄露问题的.self持有wkwebview,而在config添加的方法中 又强引用了self,造成了相互引用.这个时候我们需要两个步骤去解决它.
首先,我们需要打断这种引用关系.所以需要用weak来修饰.
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface WeakScriptMessageDelegate : NSObject<WKScriptMessageHandler>
@property (nonatomic,weak)id<WKScriptMessageHandler> scriptDelegate;
- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate;
@end
#import "WeakScriptMessageDelegate.h"
@implementation WeakScriptMessageDelegate
- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate{
self = [super init];
if (self) {
_scriptDelegate = scriptDelegate;
}
return self;
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
[self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
}
- (void)dealloc {
}
@end
通过WeakScriptMessageDelegate类,weak修饰来打破这种相互引用.
其次,在我们离开当前界面的时候,需要把添加的messagehander删除掉.
[_webView.configuration.userContentController removeScriptMessageHandlerForName:@"setappToken"];
这样就可以解决wkwebview的内存问题.
- 3 代理delegate
如果代理用strong修饰,self会强引用view,view强引用delegate.相互引用造成了内存泄露
@class QiAnimationButton;
@protocol QiAnimationButtonDelegate <NSObject>
@optional
- (void)animationButton:(QiAnimationButton *)button willStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStartAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button willStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didStopAnimationWithCircleView:(QiCircleAnimationView *)circleView;
- (void)animationButton:(QiAnimationButton *)button didRevisedAnimationWithCircleView:(QiCircleAnimationView *)circleView;
@end
@interface QiAnimationButton : UIButton
@property (nonatomic, weak) id <QiAnimationButtonDelegate> delegate;
- (void)startAnimation; //!< 开始动画
- (void)stopAnimation; //!< 结束动画
- (void)reverseAnimation; //!< 最后的修改动画
- block循环引用
并不是所有的block都能引发循环引用的.
如果blick被当前self持有,这是在block内部我们去调用self持有的方法或者属性,这时就造成了循环引用.
- block循环引用
__weak typeof(self) weakSelf = self;
[self.operationQueue addOperationWithBlock:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (completionHandler) {
KTVHCLogDataStorage(@"serial reader async end, %@", request.URLString);
completionHandler([strongSelf serialReaderWithRequest:request]);
}
}];
- 5 . 加载大图
加载大图或者多个图片的时候,使用imagenamed方法使用了系统缓存,会长时间占用内存.而使用imagewithcontentsoffile加载本地文件 不会有缓存. - 循环中对象占用内存
在循环次数比较大的时候,循环生成的对象占用内存较大.产生大量的临时对象,直到循环结束才释放.可能导致内存泄露
- 循环中对象占用内存
for (int i = 0; i<100000; i++) {
AMHomeVC *homeVC = [[AMHomeVC alloc]init];
}
for (int i = 0; i<100000; i++) {
@autoreleasepool {
AMHomeVC *homeVC = [[AMHomeVC alloc]init];
}
}
使用autoreleasepool,及时的释放占用的内存.减少内存占用峰值.
上面就是一些内存泄漏的场景.下面我们来看一下内存管理的原理实现.
内存原理
内存分布
要弄清楚ios内存管理,我们首先需要知道,我们启动一直app进程以后,它的内存是如何划分的.我们在平常的开发中需要注意的是哪块的内存管理.
代码区:代码段是用来存放可执行文件的操作命令,允许读取操作,不允许写入操作
全局静态区 ,在程序运行中,此内存一直存在,程序结束后由系统释放
1.数据区:存放程序静态分配的变量和全局变量
2 bss区 包含了程序中未初始化全局变量
常量区 常量存储区,编译时分配,在程序结束后由系统释放
堆:是由程序员分配和释放的,用于存放进程运行中被动态分配的内存段.现在使用ARC来管理对象,所以不用我们手动是realse对象.
存储的是oc中alloc或者new开辟出来的空间创建对象
优点:灵活方便 数据适用广泛 缺点:需要管理,速度慢而且容易产生内存碎片
栈:是由编译器自动分配并释放,用户存放程序临时创建的局部变量.我们是不用管栈上存储,它都是有系统来释放的.
存储的是局部变量 以及函数的参数
优点: 快速高效 不会产生内存碎片 优点 数据不灵活 内存大小啊有限制
内存管理机制
在我们启动一个app的时候,有主线程对应的也就有了autoreleasePool.
首先遇到的一个疑问就是,哪些数据会加入到autoreleasePool中?
1.非自己创建的对象:一般是系统提供的类方法/工厂方法.
这又有一个问题,什么是自己创建的对象? 自己创建的对象就是,使用alloc.new、copy、mutablecopy及其驼峰变形allocobject newobject方法创建的对象.就是开发者自己创建需要申请内存的对象.
NSArray *arr=[[NSArray alloc]init];
NSArray *testarr=[NSArray array];
上面的alloc是不会加入到autoreleasePool中,而array创建的会加入到自动释放池中.
2.函数返回值:对象作为方法的返回值 在方法里有局部对象的,除了方法作用于会立刻释放.
那接下来又有一个问题?自己创建的对象放在哪.我理解的是 他们在引用计数表中.在引用计数变为0的时候 被系统释放掉.那么,是不是可以这么总结:在点开一个app的时候,主线程 runloop对应的一个自动释放池.它管理的整个app,加入到autoreleasePool的释放问题.当然我们在项目里肯定会alloc初始化创建各个类,这些类是在引用计数表中的.但在这些类中又有各种方法 各种局部变量,他们是放在不同的autoreleasePool之中管理的.所以类中的autoreleasePool能不能释放 它也关系到这个类最后能不能释放掉.
上面说了app内存管理的管理实现,接下来开始一个一个的说明 指针存储 sidetable列表 autoreleasePool.
Tagged Pointer
苹果引入了Tagged Pointer 用来优化NSNumber,NSDate NSString等小对象的存储,在没有引入之前都是动态创建,根据引用计数来释放的.
而现在NSNumber指针里面存储的数据是:tag+data.也就是把数据直接存储到指针里面了. 我们看下面的两段代码会有一个直观的体验
dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
for (int i = 0 ; i < 10000; i ++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"%@",@"123sdfasfdas"];
});
}
上面的代码可能崩溃,因为name有可能在调用之前被释放掉.变成僵尸对象,从而引起崩溃.
dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
for (int i = 0 ; i < 10000; i ++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"%@",@"123"];
});
}
这段代码不会崩溃,这是因为他是一个小对象,只是修改指针上面的值就可以了,不存在realse释放这一说.
SideTables哈希列表
除了指针存储数据,还可以用sidetables来存储.散列表中存储的都是各个属性的引用计数.而记录引用计数的又分为了SideTable weak_table_t
SideTable 的两个成员:refcents是一个hash map,其key是objc的地址,而value则是objc对象的引用计数
weaak_table 则存储了弱引用objc的指针地址,其本质是一个以objc地址为key,弱引用objc的指针地址作为value的hash表
关于sidetable先说这么多,后续在慢慢补充吧.
自动释放池
autoreleasePool并不是一个结构体,它是由AutoreleasePoolPage组成的.
下面我们来看一下AutoreleasePoolPage是什么
结构体中包含了
magic_t const magic
id *next
pthread_t const thread
AutoreleasePoolPage *const parent
AutoreleasePoolPage *child
depth
hiwat
结构体中包含了parent child和next,page是以一个双向链表的形式连接在一起的.从网上盗用的图
截屏2022-06-24 14.49.21.png
next是最上面的 当当前的page要满了时,这时要创建新的page,next指向了新page的底部.
再看下面图所示,显示pool释放时候的过程. 截屏2022-06-24 14.49.29.png要释放的时候,加入一个哨兵对象.哨兵对象就是一个nil,只不过是换了一种形式.
根据传入的哨兵对象地址找到哨兵对象所处的page,
2.在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次release消息,并向回移动next指针到正确的位置
关于AutoreleasePool和引用列表只是简单的介绍了一下,后续在慢慢的补充.
以上就是一些 个人对内存管理的理解.