CADisplayLink
、NSTimer
会对target
产生强引用,如果target
又对它们产生强引用,那么就会引发循环引用。#import "ViewController.h"
@interface ViewController ()
@property(strong, nonatomic)CADisplayLink * link;
@property(strong, nonatomic)NSTimer * timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self linkTest];
[self timerTest];
}
//CADisplayLink
- (void)linkTest
{
// __weak typeof(self)weakSelf = self;
//这会产生循环引用,由于是内部造成的循环引用(不是block),就是有__weak修饰也是不起效果的.
// CADisplayLink保证调用频率和屏幕的刷帧频率一致,大概为60FPS
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(test)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)timerTest
{
//如果是在block中执行任务,是可以通过__weak来解决循环引用的.
// __weak typeof(self)weakSelf = self;
// self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
// [weakSelf test];
// }];
//下面NSTimer内部会对target造成循环引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
}
- (void)test
{
static int a = 0;
a++;
NSLog(@"%d",a);
}
-(void)dealloc
{
NSLog(@"%s",__func__);
//取消定时器
[self.link invalidate];
[self.timer invalidate];
}
@end
结果及分析
:上面控制器对其属性link
和timer
是强应用,link
和timer
内部又对其target
也就是控制器强引用,这就造成了循环引用(并不是block,所以用__weak修饰self是没用的),当退出控制器时,定时器任然工作,并且控制器也无法销毁。
循环引用示意图:
NSTimer
可以用block方法实现: __weak typeof(self)weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf test];
}];
CADisplayLink
是没有block
方法实现的,这是我们可以用一个中间的代理对象,定时器强引用代理,代理弱引用控制器,控制器强引用定时器;当控制器退出时就没有了强引用就销毁了,从而解除了循环应用。代理模式示意图:
设计代理
#import
@interface HJobjProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
#import "HJobjProxy.h"
@implementation HJobjProxy
+(instancetype)proxyWithTarget:(id)target{
HJobjProxy * proxy = [[HJobjProxy alloc]init];
proxy.target = target;
return proxy;
}
//消息转发(返回self.target 是说明aSelector方法由self.target去调用)
- (id)forwardingTargetForSelector:(SEL)aSelector{
return self.target;
}
@end
代理使用
//部分关键daim
- (void)linkTest
{
HJobjProxy * proxy = [HJobjProxy proxyWithTarget:self];
self.link = [CADisplayLink displayLinkWithTarget:proxy selector:@selector(test)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)timerTest
{
HJobjProxy * proxy = [HJobjProxy proxyWithTarget:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:proxy selector:@selector(test) userInfo:nil repeats:YES];
}
这样就可以完美的解决定时器循环引用的问题了。
NSProxy
是专门做这种代理用的,可以继承NSProxy
来分装一个代理。#import
@interface HJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
#import "HJProxy.h"
@implementation HJProxy
+(instancetype)proxyWithTarget:(id)target{
// NSProxy对象不需要调用init,因为它本来就没有init方法,它是一个基类,根NSObject一样没有父类,都遵守NSObject协议
HJProxy * proxy = [HJProxy alloc];
proxy.target = target;
return proxy;
}
//NSProxy 并没有forwardingTargetForSelector这个方法
//方法签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
//转发调用
- (void)forwardInvocation:(NSInvocation *)invocation{
[invocation invokeWithTarget:self.target];
}
@end
这里需要注意的是:NSProxy
并没有init
方法,也没有forwardingTargetForSelector
方法;这里面要完成消息转发需要调用两个方法,比上面更麻烦一点,但为啥还要这么做呢?这是因为第一种代理方法是需要执行消息发送
,动态方法解析
,消息转发
三大流程才能完成消息转发,而下面这种方法,直接完成了消息转发
,效率更高。
NSTimer
除了可能产生循环引用的情况,还可能产生定时不准的情况,因为NSTimer
依赖于RunLoop
,如果RunLoop
的任务过于繁重,可能会导致NSTimer不准时。而GCD的定时器会更加准时,因为GCD定时器不依赖于RunLoop
,系统自动触发,系统级别的源。 //创建定时器
static dispatch_source_t timer;
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
//设置时间,开始时间和时间间隔都是以纳秒为单位
uint64_t start = 2.0; // 2秒后开始执行
uint64_t interval = 1.0; // 每隔1秒执行
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, start * NSEC_PER_SEC, interval * NSEC_PER_SEC);
//设置回调
dispatch_source_set_event_handler(timer, ^{
static int a = 0;
a++;
NSLog(@"%d秒",a);
});
//启动定时器
dispatch_resume(timer);
//取消定时器任务
dispatch_source_cancel(timer);
注意
:全局变量或者静态变量
,否则回调不执行#import
@interface HJTimer : NSObject
/*
返回定时器唯一标识,标识和定时器一一对应,存放在全局字典中,标识是key,定时器是value
task:任务回调
start:开始时间
interval:定时间隔
repeats:是否重复
async:是否同步
*/
+ (NSString *)IdentifierWithTimerStart:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async
execTask:(void(^)(void))task;
+ (NSString *)execTask:(id)target
selector:(SEL)selector
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
//根据定时器唯一标识取消任务
+ (void)cancelTask:(NSString *)identifier;
@end
/*********实现文件*********/
#import "HJTimer.h"
@implementation HJTimer
static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//初始化一次
timers_ = [NSMutableDictionary dictionary];
//设置信号量为1保证线程安全
semaphore_ = dispatch_semaphore_create(1);
});
}
+ (NSString *)IdentifierWithTimerStart:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async execTask:(void(^)(void))task
{ //校验避免一些输入错误,如果设置不重复的话这里的时间间隔就是无效的可以鼠标输入
if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
// 队列:同步就是主队列,异步就是并发队列
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置时间
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);
//保证线程安全
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
// 定时器的唯一标识
NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
NSLog(@"name%",name);
// 存放到字典中
timers_[name] = timer;
dispatch_semaphore_signal(semaphore_);
// 设置回调
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeats) { // 不重,任务调一次就取消任务
[self cancelTask:name];
}
});
// 启动定时器
dispatch_resume(timer);
return name;
}
+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!target || !selector) return nil;
return [self IdentifierWithTimerStart:(NSTimeInterval)start interval:interval repeats:repeats async:async execTask:^{
if ([target respondsToSelector:selector]) {
//去掉警告⚠️
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:selector];
#pragma clang diagnostic pop
}
}];
}
//取消任务
+ (void)cancelTask:(NSString *)identifier
{
if (identifier.length == 0) return;
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
dispatch_source_t timer = timers_[identifier];
if (timer) {
//取消任务
dispatch_source_cancel(timer);
//移除定时器
[timers_ removeObjectForKey:identifier];
}
dispatch_semaphore_signal(semaphore_);
}
@end
调用
:
- (void)viewDidLoad {
[super viewDidLoad];
// [self linkTest];
// [self timerTest];
// [GCDTimer test];
self.timer1 = [HJTimer IdentifierWithTimerStart:1 interval:1 repeats:YES async:YES execTask:^{
static int a = 0;
a++;
NSLog(@"A:%d--%@",a,[NSThread currentThread]);
}];
self.timer2 = [HJTimer execTask:self selector:@selector(test) start:1 interval:1 repeats:YES async:NO];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[HJTimer cancelTask:self.timer1];
[HJTimer cancelTask:self.timer2];
}
封装后的GCD定时器使用非常简单方便,不会引起循环引用问题,并且定时也非常准确。
代码段
:编译之后的代码
数据段
字符串常量:比如NSString *str = @“123”
已初始化数据:已初始化的全局变量、静态变量等
未初始化数据:未初始化的全局变量、静态变量等
堆
:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大
栈
:函数调用开销,比如局部变量。分配的内存空间地址越来越小
从64bit开始,iOS引入了Tagged Pointer
技术,用于优化NSNumber、NSDate、NSString
等小对象的存储。
在没有使用Tagged Pointer
之前, NSNumber
等对象需要动态分配内存、维护引用计数
等,NSNumber
指针存储的是堆中NSNumber
对象的地址值。
使用Tagged Pointer
之后,NSNumber
指针里面存储的数据变成了:Tag + Data
,也就是将数据直接存储在了指针中
。
当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。
objc_msgSend
能识别Tagged Pointer
,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销
。
如何判断一个指针是否为Tagged Pointer
?
iOS平台,最高有效位是1(第64bit),Mac平台,最低有效位是1。
示例:
NSNumber *number1 = @4;
NSNumber *number2 = @5;
NSNumber *number3 = @10;
NSNumber *number4 = @(0xFFFFFFFFFFFFFFF);
NSLog(@"num:\n%p\n%p\n%p\n%p\n", number1, number2, number3,number4);
NSString * str1 = [NSString stringWithFormat:@"1"];
NSString * str2 = [NSString stringWithFormat:@"2"];
NSString * str3 = [NSString stringWithFormat:@"8"];
NSString * str4 = [NSString stringWithFormat:@"sadasdasdasdsasdasdsa"];
NSLog(@"str:\n%p\n%p\n%p\n%p\n", str1, str2, str3,str4);
结果可以看出:number1、number2、number3
地址很小8字节,是将值放在指针中的,number4
地址很大16字节,因为值比较大指针里面放不下,所以放在了堆中,字符串也是一样,字符串是字符对应的阿斯克码。
引用计数
来管理OC对象的内存;一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
。retain
会让OC对象的引用计数+1,调用release
会让OC对象的引用计数-1。alloc、new、copy、mutableCopy
方法返回了一个对象,在不需要这个对象时,要调用release
或者autorelease
来释放它。extern void _objc_autoreleasePoolPrint(void);
alloc
等方法创建后retainCount
为1,所以在使用完后必须调用release
,或者在创建的时候就调用autorelease
方法。 NSString * str = [[NSString alloc] initWithString:@"abcdefghijklmnaaaaaaaa"];
[str release];
NSString * str2 = [[[NSString alloc] initWithString:@"abcdefghijklmnaaaaaaaa"] autorelease];
HJPerson
并且有个自定义HJDog
类型的dog
属性。#import
#import "HJDog.h"
@interface HJPerson : NSObject{
HJDog * _dog;
}
- (void)setDog:(HJDog *)dog;
- (HJDog *)dog;
@end
主要研究其实现:
实现方式1:
//属性的实现
-(void)setDog:(HJDog *)dog{
_dog = dog;
NSLog(@"dog1:%p",dog);
}
-(HJDog *)dog{
return _dog;
}
//使用
HJDog * dog = [[HJDog alloc]init];
HJPerson *person1 = [[HJPerson alloc] init];
[person1 setDog:dog];
[dog release];
//坏内存访问
[[person1 dog] run];
[person1 release];
可以看出person1
没有销毁并且也给其属性dog
设了值,但由于dog
已经释放,这时候调用person1
的dog
属性run
就会报坏内存访问的错误。如果person1
是个全局变量,属性这样设置,release就没了,这样的属性就没有意义了。所以设置属性时应该将其retainConut
加1。
实现方式2:
//主要改了setter方法,getter方法不变
-(void)setDog:(HJDog *)dog{
_dog = [dog retain];
}
-(void)dealloc{
[_dog release];
NSLog(@"%s",__func__);
[super dealloc];
}
//调用
HJDog * dog = [[HJDog alloc]init];//1
HJDog * dog2 = [[HJDog alloc]init];//1
HJPerson *person1 = [[HJPerson alloc] init];
[person1 setDog:dog];//2
[person1 setDog:dog2];//2
[dog release];//1
[dog2 release];//1
[person1.dog run];
//内存泄漏:该释放的对象没有释放
[person1 setDog:dog];
[person1 release];//这时候只有dog2的引用计数为0,dog的引用计数还是1,
在setter
方法中调用retain
,在HJPerson
的方法中调用release
,这样就能保证创建之后HJPerson
销毁时才销毁。但是多次设置属性时就会出现问题 ,person1
销毁是_dog
调用release
只能把最后一次赋值给属性的变量的引用计数值减1变为0,前面多次调用setter
方法传进来的变量都没有减1,前面这些变量都没法销毁,就造成了内存泄漏。
实现方式3:
-(void)setDog:(HJDog *)dog{
if (_dog != dog) {
[_dog release];
_dog = [dog retain];
}
}
-(void)dealloc{
// [_dog release];
// _dog = nil;
self.dog = nil;
NSLog(@"%s",__func__);
[super dealloc];
}
判断每次传进来的对象是不是同一个,是就不做操作,不是就先把以前的对象release
释放掉,然后再retain
引用着,最后在dealloc
要将最后一次赋值的属性release
释放掉,为了避免僵尸对象,将属性对应的成员变量的指针指向nil
,调用setter
方法并将nil
传进去也达到了同样的目的。
注意
:不管是arc
还是mrc
环境,定义属性只需要用@property
定义即可,不需要再重写setter、getter
方法,系统已经自动生成了,但要在dealloc
方法中将属性置为nil
。
注意2
:只要不是alloc、new、copy、mutableCopy
创建的对象都不需要调用release
或autorelease
,方法内部已经调用了,例如:
NSString * str = [NSString stringWithFormat:@"asdasdasdasdaadsadas"];
//报坏内存访问
// [str release];
NSString * str2 = [NSString string];
NSArray * arr = [NSArray array];
NSString * str1 = [[NSString alloc] initWithFormat:@"asdasdasdsadsa"];
NSString * str2 = [str1 copy];
// NSString * str2 = [str1 retain];
NSMutableString * str3 = [str1 mutableCopy];
[str3 appendString:@"1111"];
NSLog(@"\n%p--%@--%ld\n%p--%@--%ld\n%p--%@--%ld",str1,str1,[str1 retainCount],str2,str2,[str2 retainCount],str3,str3,[str3 retainCount]);
[str1 release];
[str2 release];
[str3 release];
copy
属性实现类似retain
属性实现:- (void)setData:(NSArray *)data
{
if (_data != data) {
[_data release];
_data = [data copy];
}
}
- (void)dealloc
{
self.data = nil;
[super dealloc];
}
关于拷贝详解可参看我的另一篇文章:iOS底层原理之OC语法
(深拷贝和浅拷贝)
前面runtime
中已经讲过在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable
类中:
refcnts
是一个存放着对象引用计数的散列表
当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是
dealloc
_objc_rootDealloc
ootDealloc
object_dispose
objc_destructInstance、free
weak
关键字 // ARC是LLVM编译器和Runtime系统相互协作的一个结果
__strong HJPerson *person1;
__weak HJPerson *person2;
__unsafe_unretained HJPerson *person3;
NSLog(@"begin");
{
HJPerson *person = [[HJPerson alloc] init];
// person1 = person;
// person2 = person;
person3 = person;
}
NSLog(@"end - %@", person3);
}
可以看出如果是__strong
,person
会在end
之后释放;如果是__weak
,person
会在begin
之后end
之前释放,然后person2
会指向nil;如果是__unsafe_unretained
,person
也会在begin
之后end
之前释放,但是person3
依旧指向被释放了的对象的内存,打开僵尸监听可以看见报坏内存访问,及产生了僵尸指针。
weak
和assign的区别就在于此,
weak安全的,指向的对象释放后,指针指向
nil,
assign`依然指向被释放的内存,可能产生僵尸对象。
weak
的原理就是在调用dealloc
运行时,isa
中会判断有没有弱引用,弱引用是存储在一个哈希表中,如果有就将弱引用就会根据当前对象的地址在去哈希表中去找到对应的弱引用并清除弱引用。
__AtAutoreleasePool、AutoreleasePoolPage
;调用了autorelease
的对象最终都是通过AutoreleasePoolPage
对象来管理的。clang重写@autoreleasepool
objc4源码:NSObject.mm
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
AutoreleasePoolPage
对象占用4096
字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease
对象的地址;所有的AutoreleasePoolPage
对象通过双向链表的形式连接在一起。push
方法会将一个POOL_BOUNDARY
入栈,并且返回其存放的内存地址;pop
方法时传入一个POOL_BOUNDARY
的内存地址,会从最后一个入栈的对象开始发送release
消息,直到遇到这个POOL_BOUNDARY
;id *next
指向了下一个能存放autorelease
对象地址的区域。#import "ViewController.h"
#import "HJPerson.h"
@interface ViewController ()
@end
//系统方法,用来打印autoreleasepool
extern void _objc_autoreleasePoolPrint(void);
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
HJPerson *p1 = [[[HJPerson alloc] init] autorelease];
HJPerson *p2 = [[[HJPerson alloc] init] autorelease];
@autoreleasepool {
HJPerson *p3 = [[[HJPerson alloc] init] autorelease];
@autoreleasepool {
HJPerson *p4 = [[[HJPerson alloc] init] autorelease];
}
@autoreleasepool{
HJPerson *p5 = [[[HJPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint();
}
}
}
}
因为p4
地址已经移除了所以只打印出四个Person
对象地址,其中###
是代码POOL_BOUNDARY
参数。如果对象太多不够存储就会再新增一个AutoreleasePoolPage
。对象清空后AutoreleasePoolPage
也就销毁了。
Runloop
中注册了2个Observer
:第1个Observer
监听了kCFRunLoopEntry
事件(进入runloop),会调用objc_autoreleasePoolPush()
,即将对象地址保存到AutoreleasePoolPage当中;第2个Observer
监听了kCFRunLoopBeforeWaiting
事件(runloop休眠),会调用objc_autoreleasePoolPop()
、这时候就根据对象的地址调用release、objc_autoreleasePoolPush()
;kCFRunLoopBeforeExit
事件(runloop退出),会调用objc_autoreleasePoolPop()
。- (void)viewDidLoad {
[super viewDidLoad];
// 这个Person什么时候调用release,是由RunLoop来控制的
// 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release
HJPerson *person = [[[HJPerson alloc] init] autorelease];
//HJPerson *person = [[HJPerson alloc] init];
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
可以看出person
对象并不是在viewDidLoad
中释放的,二而是在viewWillAppear
之后viewDidAppear
之前释放的,这是因为viewDidLoad
和viewWillAppear
是在RunLoop
休眠之前加入的事件如source1
,所以这时候并没有调用release
。
#import "ViewController.h"
@interface ViewController ()
@property(strong,nonatomic)NSString *name;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//思考以下2段代码能发生什么事?有什么区别?
//循环1
for (int a = 0; a < 1000; a++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.name = [NSString stringWithFormat:@"abcdefghijk"];
});
}
//循环2
for (int a = 0; a < 1000; a++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.name = [NSString stringWithFormat:@"abcd"];
});
}
}
结果
:循环1会报坏内存访问的错误,循环2不会
;
原因
:因为字符串“abcdefghijk”值比较大,里面是通过alloc
创建的,在name
的setter
方法中每次判断不是同一个变量时会调用[_name release]
方法,但是由于是是异步并发执行的,可能同时release
多次,就造成了坏内存访问(当第一次release
时,_name指向的对象已经销毁,现在就是一个野指针,第二再给这个野指针发生release
的时候就会报坏内存访问);而字符串"abcd"的值比较小,是通过Tagged Pointer
的方式存储的,在name
的setter
方法中会判断如果是这种方式就不会进行release
操作,故不会发生坏内存访问的问题。
解决方案
:方案1用atomic
修饰属性,方案2在设置name
时加锁设置完后解锁。
target
的循环引用,造成内存泄露。alloc、copy、new
创建对象时,对象的引用计数为1,当调用retain
时,引用计数加1,当调用release
时,引用计数减1,当引用计数为0时,对象销毁,释放对象内存。weak
引用,如果有会在调用dealloc方法时清空weak
引用,将weak
引用的对象引用计数减1,并将weak
指向nil
避免参数僵尸对象。weak
的原理就是在调用dealloc
运行时,isa
中会判断有没有弱引用,弱引用是存储在一个哈希表中,如果有就将弱引用就会根据当前对象的地址在去哈希表中去找到对应的弱引用并清除弱引用。release
,如果是在Runloop
中,则是由是由RunLoop来控制的,RunLoop休眠之前调用了release。autorelease
则不一定会立即释放,RunLoop休眠之前调用了release,然后释放对象,看编译器插入的是什么内存管理代码。