当前位置: 首页 > news >正文

【iOS】ARC学习

前文:引用计数

请先看我的这篇文章:
引用计数

内存管理

1.1 alloc/retain/release/dealloc的实现

在这里,我们使用开源软件GNUstep(GNUstep的源代码虽不能说与苹果的Cocoa完全相同,胆识从使用者角度来看,两者的行为和实现方法是一样的。)来说明。

alloc实现

在Objective-C中,对象的创建和初始化通常是由类的工厂方法和初始化方法来完成的。这些方法的实现通常包含了为对象分配内存和初始化对象的代码。

以NSObject类的alloc方法为例,其源代码如下:

+ (instancetype)alloc {
    return [self allocWithZone:NULL];
}

这个方法是一个类方法,返回一个新分配的对象。它实际上是调用了类的allocWithZone:方法并传入了一个NULL的参数。

而allocWithZone:方法的源代码如下:

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return NSAllocateObject(self, 0, zone);
}

这个方法也是一个类方法,返回一个新分配的对象。它实际上是调用了NSAllocateObject函数来为对象分配内存。

关于NSAllocateObject函数,其实现如下:

struct obj_layout {
	NSUInteger retained;
};

inline id;
NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone)
{
	int size = 计算容纳对象所需内存的大小;
	id new = NSZoneMalloc(Zone,size);
	memset (new, 0, size);
	new = (id)&((struct obj_layout *) new)[1];
}

NSAllocateObject函数通过调用NSZoneMalloc函数来分配存放对象所需要的内存空间,之后将空间置0,最好返回作为对象使用的指针。

以下是去掉NSZone后简化了的源代码:

struct obj_layout {
	NSUInteger retained;
};

+ (id) alloc {
	int size = sizeof(struct obj_layout) + 对象大小;
	struct obj_layout *p = (struct obj_layout *)calloc(1, size);
	return (id)(p+1); 
}

alloc类方法用struct obj_layout 中的retained整数来保存引用计数,并将其写入对象内存头部,该对象内存块全部置0后返回。以下用图示来展示有关GNUstep的实现。
在这里插入图片描述

retain

对象的引用计数可用retainCount实例方法获得。
下面是GNUstep源代码。

- (NSUInteger) retainCount
{
	return NSExtraRefCount (self) + 1;
}

inline NSUInteger 
NSExtraRefCount (id anOnject)
{
	return ((struct obj_layout *) anObject)[-1].retained;
}

由地址寻找到对象内存头部,从而访问其中的retained变量。

在这里插入图片描述

因为分配时全部置0,所以retained 为0。由NSExtraRefCount(self)+1得出,retainCount为1。可以推测出retain方法使retained变量加1,而release方法使retained变量减1。

[obj retain];

下面看一下像上面那样调用出的retain实例方法。

- (id) retain 
{
	NSIncermentExtraRefCount(self);
	return self;
}

inline void
NSIncrementExtraRefCount(id anObject)
{
	if (((struct obj_layout *)anObject[-1].retained == UINT_MAX - 1)
	[NSException raise:NSInternalInconsistencyException format:@"NSIncrementExtraRefCount () asked to increment too far"]);
	((struct obj_layout *) anObject)[-1].retained++;
}

这段代码实现了 Objective-C 对象的引用计数机制中 retain 方法的具体实现。当我们调用一个对象的 retain 方法时,其引用计数值会加1,表示有一个新的对象持有了该对象的引用,从而防止该对象在被其它对象持有的引用都被释放后被系统回收。

具体实现中,retain 方法内部会调用 NSIncrementExtraRefCount 函数来实现引用计数的增加。NSIncrementExtraRefCount 函数会将传入的对象的 retained 域加1,该域存储了当前对象被引用的次数。如果 retained 域已经达到最大值(即 UINT_MAX - 1),则会抛出一个内部不一致的异常。

在这段代码中,通过将传入的对象指针 anObject 转换为结构体指针 ((struct obj_layout *) anObject),然后访问其前一个元素 [-1],即访问对象所在内存块的前一个元素,这个前一个元素就是对象的布局结构体,通过访问这个布局结构体的 retained 域,可以得到当前对象的引用计数值,进而实现对其进行增加的操作。

release

以下为release方法的实现。

- (id) release
{
	if (NSDecrementExtraRefCountWasZero(self))
		[self dealloc];
}

BOOL
NSDecrementExtraRefCountWasZero(id anObject)
{
	if (((struct obj_laout *) anObject)[-1].retained == 0) {
		return YES;
	} else {
		((struct obj_laout *) anObject)[-1].retained--;
		return NO;
	}
}

这段代码实现了 Objective-C 对象的引用计数机制中 release 方法的具体实现。当retained变量大于0时减1,等于0时调用dealloc实例方法,废弃对象。

具体引用计数的访问与retain方法相同。

dealloc

以下为delloc实例方法的实现。

- (void)dealloc 
{
	NSDealloocateObject(self);
}
inline void
NSDeallocateObject(id anObject)
{
	struct obj_layout *o = &((struct obj_layout *)anObject0[-1];
	free(o);
}

上述代码仅废弃由alloc分配的代码块。

1.2 苹果的实现

因为NSObject类的源代码没有公开,此处利用Xcode的调试器和iOS大概追溯出其实现过程。

在NSObject类的alloc类方法上设置断点,追踪程序的执行。以下列出了执行所调用的方法和函数。

+alloc
+allocWithZone:
class_createInstance
calloc

其中:
class_createInstance 是一个 Objective-C Runtime API,用于创建一个指定类的实例对象。它的声明如下:

id class_createInstance(Class cls, size_t extraBytes);

其中,cls 是要创建实例对象的类,extraBytes 是要为这个对象分配的额外字节数,通常情况下这个参数为 0。函数返回一个指向新创建实例对象的指针。

其中调用了calloc函数分配内存块。

同样的方法,看一下retainCout/retain/release实例方法实现。

-retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
-retain
__CFDoExternRefOperation
CFBasicHashAddValue
-release
__CFDoExternRefOperation
CFBasicHashRemoveValue
(CFBasicHashRemoveValue 返回0时,-release调用dealloc)

其中:
__CFDoExternRefOperation 是 Core Foundation 框架中的一个 C 函数,用于执行外部引用计数操作。在 Core Foundation 中,有一类对象叫做外部引用对象(External References),这些对象的引用计数不由 Core Foundation 管理,而是由外部程序来管理。当 Core Foundation 操作这些对象时,需要调用外部程序提供的函数来对对象的引用计数进行操作,这就是 __CFDoExternRefOperation 函数的作用。

__CFDoExternRefOperation 函数的声明如下:

void __CFDoExternRefOperation(CFTypeRef cf, void (*op)(CFTypeRef cf1, void *context), void *context, Boolean isDeallocating);

该函数接收四个参数:

cf:外部引用对象的指针。
op:指向外部程序提供的引用计数操作函数的指针。这个函数接收两个参数,第一个参数是外部引用对象的指针,第二个参数是上下文信息指针。
context:指向上下文信息的指针,传递给 op 函数。
isDeallocating:表示是否正在释放对象的标志。
在 __CFDoExternRefOperation 函数中,我们首先对传入的参数进行了非空判断,然后根据 isDeallocating 参数的值选择调用外部程序提供的引用计数操作函数 op 的不同版本。如果 isDeallocating 参数为 true,就调用 op 函数的释放版本,否则就调用 op 函数的保留版本。在调用 op 函数之前,我们先对对象进行了锁定操作,防止在操作期间发生对象被释放的情况。

需要注意的是,__CFDoExternRefOperation 函数只是一个内部函数,外部程序一般不会直接调用它。外部程序通常会使用 Core Foundation 提供的一些宏或函数来管理外部引用对象的引用计数,例如 CFMakeExternalRefCounted()、CFRetain() 和 CFRelease() 等函数。

下面是简化了的__CFDoExternRefOperation函数的源代码。

int  __CFDoExternRefOperation(uintptr_t op, id obj) {
	CFBasicHashRef table = 取得对像对应的散列表(obj);
	int count;
   switch(op) {
   	case OPERATTON_retainCount:
   		count = CFBasicHashGetCountOfKey(table, obj);
   		return count;

   	case OPERATTON_retain:
   		CFBasicHashAddValue(table, obj);
   		return obj;
   		
   	case OPERATTON_release:
   		count = CFBasicHashRemoveValue(table, obj);
   		return 0 == count;
   }
}

__CFDoExternRefOperation函数按retainCount/retain/release操作进行分发,调用不同的函数。NSObject类的retainCount/retain/release实例方法也许如下面代码所示。

- (NSUInteger) retainCount 
{
	return (NSInteger) __CFDoExternRefOperation(OPERATTON_retainCount, self);
}

- (id) retain
{
	return (id) __CFDoExternRefOperation(OPERATTON_retain, self);
}

- (void) release
{
	return  __CFDoExternRefOperation(OPERATTON_release, self);
}

可以从__CFDoExternRefOperation函数以及由此函数调用的各个函数名来看,苹果的实现大概就是采用散列表(引用计数表)来管理引用计数。如图:
在这里插入图片描述

这样实现与GNUstep相比的好处:

通过内存块头部管理引用计数的好处如下:

  • 少量代码即可完成
  • 能够统一管理引用计数内存块与对象的内存块

通过引用计数表管理引用计数的好处如下:

  • 对象用内存块的分配无需考虑内存块头部。
  • 引用计数表各记录中存有内存地址,可从各个记录追溯到各对象的内存块。

另外,在利用工具检测内存泄露时,引用计数表的各记录也有助于检测各对象的持有者是否存在。

1.3 autorelease

autorelease方法可以使取得的对象存在,但自己不持有对象。autorelease提供这样的功能,是对象在超出指定的生存范围时能够自动并正确地释放(release方法)。

另外,编程人员可以设定变量的作用域。

autrelease的具体使用方法如下:

  • 生成并持有NSAutoreleasePool对象;
  • 调用已分配对象的autorrelease实例方法;
  • 废弃NSAutoreleasePoll对象。

在这里插入图片描述

在Cocoa框架中,相当于程序主循环的NSRunLoop或者在其他程序可运行的地方,对NSAutoreleasePool对象进行生成、持有和废弃处理。

在这里插入图片描述

1.4 autorelease实现

autorelease的实现。GNUstep的源代码。

[obj autorelease];

此源代码调用NSObject类的autorelease实例方法。

- (id) autorelease
{
	[NSAutoreleasePool addObject:self];
}

autorelease实例方法的本质就是调用NSAutoreleasePool对象的addObject类方法。

下面看NSAutoreleasePool类的实现。由于NSAutoreleasePool类的源代码比较复杂,所以我们假象一个简化的源代码进行说明。

+ (void) addObject:(id)anObj
{
	NSAutoreleasePool *pool = 取得正在使用的NSAutoreleasePool对象;
	if (pool != nil) {
		[pool addObject:anObj];
	} else {
		NSLog(@"NSAutoreleasePool对象非存在状态下调用autorelease");
	}
}

addObject类方法调用正在使用的NSAutoreleasePool对象的addobject实例方法。以下源代码中,被赋予pool变量的即为正在使用的NSAutoreleasePool对象。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];

如果嵌套生成或持有的NSAutoreleasePool对象,理所当然会使用最内侧的对象。下例中,pool2为正在使用的NSAutoreleasePool对象。

NSAutoreleasePool *pool0 = [[NSAutoreleasePool alloc] init];
	NSAutoreleasePool *pool1 = [[NSAutoreleasePool alloc] init];
		NSAutoreleasePool *pool2 = [[NSAutoreleasePool alloc] init];

		id obj = [[NSObject alloc] init];
		[obj autorelease];

		[pool2 drain];
	[pool1 drain];
[pool0 drain];

下面看一下addObject实例方法的实现。

- (void) add object:(id) anObj
{
	[array addObject:anObj];
}

实际的GNUstep实现使用的是连接列表,这同在NSMutableArray对象中追加对象参数是一样的。

如果调用NSObject类的autorelease实例方法,该对象将被追加到正在使用的NSAutoreleasePool对象的数组里。

[pool drain];

以下为通过drain实例方法废弃正在使用的NSAutoreleasePool对象的过程。

- (void) drain 
{
	[self dealloc];
}

- (void) dealloc
{
	[self emptyPool];
	[array release];
}

- (void) emtyPool
{
	for (id obj in array) {
		[obj release];
	}
}

相关文章:

  • 3. QML实现蓝牙通信
  • 算法第二十期——FLoyd算法的入门与应用
  • VBA之正则表达式(41)-- 替换函数声明
  • python get方法及常用的代码
  • Vue——插槽
  • uni-app的基本使用(二)
  • kubeSphere / k8s中master、worker节点启停命令操作
  • Vue内容分发
  • MySQL主从复制、读写分离(MayCat2)实现数据同步
  • C#,码海拾贝(04)——拉格朗日(Lagrange)曲线插值算法及其拓展,《C#数值计算算法编程》源代码升级改进版
  • 解决使用WinScp连接Ubantu系统失败的问题---SSH无法连接
  • ChatGPT会干掉测试吗?
  • 使用Go语言编写命令行实用程序
  • rk3568-AD按键驱动调试
  • linux操作系统基础(含C编译,make编译,shell脚本)
  • [rust! #004] [译] Rust 的内置 Traits, 使用场景, 方式, 和原因
  • [数据结构]链表的实现在PHP中
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • bearychat的java client
  • JavaScript 无符号位移运算符 三个大于号 的使用方法
  • JavaScript函数式编程(一)
  • Redis提升并发能力 | 从0开始构建SpringCloud微服务(2)
  • Storybook 5.0正式发布:有史以来变化最大的版本\n
  • vue2.0开发聊天程序(四) 完整体验一次Vue开发(下)
  • 纯 javascript 半自动式下滑一定高度,导航栏固定
  • 代理模式
  • 订阅Forge Viewer所有的事件
  • 多线程 start 和 run 方法到底有什么区别?
  • 飞驰在Mesos的涡轮引擎上
  • 基于 Ueditor 的现代化编辑器 Neditor 1.5.4 发布
  • 近期前端发展计划
  • 一些css基础学习笔记
  • 自动记录MySQL慢查询快照脚本
  • 扩展资源服务器解决oauth2 性能瓶颈
  • ​卜东波研究员:高观点下的少儿计算思维
  • # Swust 12th acm 邀请赛# [ K ] 三角形判定 [题解]
  • # 睡眠3秒_床上这样睡觉的人,睡眠质量多半不好
  • (16)UiBot:智能化软件机器人(以头歌抓取课程数据为例)
  • (免费领源码)python+django+mysql线上兼职平台系统83320-计算机毕业设计项目选题推荐
  • (一)RocketMQ初步认识
  • (转) 深度模型优化性能 调参
  • *p++,*(p++),*++p,(*p)++区别?
  • .htaccess配置常用技巧
  • .net core 6 redis操作类
  • .Net 访问电子邮箱-LumiSoft.Net,好用
  • .net通用权限框架B/S (三)--MODEL层(2)
  • .NET下ASPX编程的几个小问题
  • /bin/rm: 参数列表过长"的解决办法
  • @Data注解的作用
  • []T 还是 []*T, 这是一个问题
  • [BZOJ3211]:花神游历各国(小清新线段树)
  • [Bzoj4722]由乃(线段树好题)(倍增处理模数小快速幂)
  • [C++] new和delete
  • [Hibernate] - Fetching strategies
  • [HJ73 计算日期到天数转换]