面试系列:
-
iOS全解1:基础/内存管理/Block/GCD(
当前位置
) - iOS全解2:Runloop
- iOS全解3:Runtime
- iOS全解4:KVC/KVO、通知/推送/信号量、Delegate/Protocol、Singleton
- iOS全解5:网络协议 HTTP、Socket
- iOS全解6:CoreAnimation/Layer
- iOS全解7:音频/视频
- iOS全解8:启动优化、性能优化、App后台保活、崩溃检测
- iOS全解9:编程思想、架构、组件化、RAC
基本概念
一、OC的三大特性为:封装
、继承
、多态
1、封装:
- 成员变量的封装:setter、getter 方法,self调用方便高效
- 普通方法的封装:简洁、高效,不用关心内部过程
- SDK的封装:使用方便、私有隐藏,安全性高(不会泄露数据和代码)
2、继承
- 抽取了重复代码:子类可以拥有父类中的所有成员变量和方法
- 建立了类与类之间的联系
- 每个类中都有一个superclass指针指向父类
- 所有类的根类都是NSObject,便于isa指针 查询
- 调用某个对象的方法时,优先去当前类中找,如果找不到,再去父类中找
- 子类重新实现父类的某个方法,会覆盖父类以前的方法。
继承的缺点:耦合性太高(类与类之间的关系过于紧密),没有多继承,OC里面都是单继承,多继承可以用protocol委托代理来模拟实现;可以通过实现多个接口完成OC的多重继承。
3、多态
例如: -(void) animal:(id); id 就是多态,传入时,才识别 具体类的真实形象。runtime 也是运行时,才识别具体类。
要想使用多态必须使用继承(继承是多态的前提)
多态:父类指针指向子类对象 Animal *aa = [Dog new]; 调用方法时会检测对象的真实形象
好处:如果函数或方法参数中使用的是父类类型,可以传入父类,子类对象。
局限性:父类类型的变量不能直接调用子类特有的方法,必须强制转换为子类类型变量后,才能使用。
property的关键字分三类:
1.原子性(也就线程安全)
有atomic和nonatomic, acomic就是线程安全,但是一般使用nonacomic,因为acomic的线程安全开销太大,影响性能,即使需要保证线程安全,我们也可以通过自已的代码控制,而不用acomic。
2.引用计数:
assign:
assign用于非指针变量,基本修饰数据类型,统一由系统的栈区进行内存管理。(int、double、float等)
weak:
对对象的弱引用,不增加引用计数,也不持有对象,当对象消失后指针自动变为nil。
copy:分为深拷贝、浅拷贝,可变复制、不可变复制
浅拷贝
:对内存地址的复制,让目标对象指针和原对象指向同一片内存空间会增加引用计数
深拷贝
:对对象内容的复制,开辟新的内存空间
单层深复制
,也就是我们经常说的深复制,我这里说的单层深复制是对于集合类所说的(即NSArray、NSDictionary、NSSet),单层深复制指的是只复制了该集合类的最外层,里边的元素没有复制,(即这两个集合类的地址不一样,但是两个集合里所存储的元素的地址是一样的)
完全复制
,指的是完全复制整个集合类,也就是说两个集合地址不一样,里边所存储的元素地址也不一样
引用:深拷贝和浅拷贝,可变复制,不可变复制(这里解释更详细)https://www.jianshu.com/p/e3da07684bf1
- 非集合类(NSString,NSNumber)
[noMutableObject copy] //浅复制 (复制不可变对象)
[noMutableObject mutableCopy] //深复制 (复制不可变对象)
[mutableObject copy] //深复制 (复制可变对象)
[mutableObject mutableCopy] //深复制 (复制可变对象)
- 集合类(NSArray,NSDictionary, NSSet)
[noMutableObject copy] //浅复制 (复制不可变对象)
[noMutableObject mutableCopy] //单层深复制 (复制不可变对象)
[mutableObject copy] //单层深复制 (复制可变对象)
[mutableObject mutableCopy] //单层深复制 (复制可变对象)
- 那么如何实现多层复制呢?我们以NSArray举例说明
// 完全复制
NSArray *copyArray = [[NSArray alloc] initWithArray:array copyItems:YES];
需要特别注意的是
以上我们所说的两种情况默认都实现了NSCopying
和NSMutableCopying
协议。
对于自定义继承自NSObject的类
copy需要实现NSCopying协议,然后实现以下方法,否则copy会crash
-(id)copyWithZone:(NSZone *)zone {
CopyObject *copy = [[[self class] alloc] init];
copy.name = self.name;
copy.mobile = self.mobile;
copy.company = self.company;
copy.descInfo = self.descInfo;
return copy;
}
mutableCopy时,需要实现NSMutableCopying协议,否则mutableCopy会crash
-(id)mutableCopyWithZone:(NSZone *)zone {
MutableCopyObject *mutableCopy = [[[self class] alloc] init];
mutableCopy.name = self.name;
mutableCopy.mobile = self.mobile;
mutableCopy.company = self.company;
mutableCopy.descInfo = self.descInfo;
return mutableCopy;
}
strong:浅拷贝,也就是指针引用
是每对这个属性引用一次,retainCount 就会+1,只能修饰 NSObject 对象,不能修饰基本数据类型。是 id 和 对象 的默认修饰符。
unsafe_unretained:和week 非常相似
可以同时修饰基本数据类型和 NSObject 对象 ,其实它本身是 week 的前身 , 在 iOS5 之后,基本都用 week 代替了 unsafe_unretained 。 但它们之间还是稍微有点区别的,并不是完全一样,对上层代码来说,能用 unsafe_unretained 的地方,都可以用 week 代替。“同时要注意一点,这个修饰符修饰的变量不属于编译器的内存管理对象”
二、类别Category
重写一个类的方式用「继承」还是「分类」,取决于具体情况:
- 假如目标类有许多的子类,我们需要拓展这个类又不希望影响到原有的代码,继承后比较好。
- 如果仅仅是拓展方法,分类更好.(不需要涉及到原先的代码)
优点:
- 分类中方法的优先级比原来类中的方法高,也就是说,在分类中重写了原来类中的方法,那么分类中的方法会覆盖原来类中的方法。+(void)load方法是一个特例,它会在当前类执行完之后,category中的再执行。)
- 可以用runtime进行method swizzling(方法的偷梁换柱)来处理异常调用的方法。
缺点:
通过观察头文件我们可以发现,Cocoa框架中的许多类都是通过category来实现功能的,可能不经意间你就覆盖了这些方法中的其一,有时候就会产生一些无法排查的异常原因。
其他问题
浅拷贝和深拷贝的区别?
浅拷贝
:只复制指向对象的指针,指针指向同一个地址,而不复制引用对象本身。
深拷贝
:复制引用对象本身。内存中存在了两份独立对象本身,当修改A 时,B 不变。(即:B = [A copy]; )
一、内存管理
案例:
办公室开关灯
,有人开灯,无人关灯;进人加一,出人减一;第一个开灯的人持有此灯,第一个人能废弃此灯,其他人只是弱引用。
内存管理原则:
1.自己生成的对象,自己持有
2.非自己生成的对象,自己也能持有
3.不再需要自己持有的对象时,释放掉
4.非自己持有的对象,无法释放(释放会崩溃)
对象操作与Objective-C方法的对应
对象操作 | Objective-C方法 |
---|---|
生成并持有对象 | alloc、new、copy、mutableCopy |
持有对象 | retain (引用计数加1) |
释放对象 | release(引用计数减1) |
废弃对象 | dealloc(引用计数为0) |
相关释放函数:autorelease、NSAutoreleasePool、@autoreleasepool
2、非自己生成的对象,自己也能持有
id obj = [NSMutableArray array];
[obj retain];
* 使用autorelease方法,可以取得对象的存在,但是自己不持有对象(obj不立刻释放掉,注册到autoreleasePool中)
- (id)object {
id obj = [[NSObject alloc] init];
[obj autorelease];
return obj;
}
ARC 环境下的使用功能规则
- 不能使用retain、release、retainCount、autorelease
- 不能使用NSAllocateObject、NSDeallocateObject
- 必须遵循内存管理命名规则(驼峰命名法)
- 不要显式调用dealloc
- 使用 @autorelease 代替 NSAutoreleasePool
- 不能使用区域 (NSZone)
- 对象型变量不能作为C语言结构体(struct/union)的成员
- 显式转换 “id” 和 “void *”
会让对象引用计数增加的操作:
1. new、alloc、retain、copy、mutableCopy
2. 添加视图:用 addview 把一个控件添加到视图上,这个控件的引用计数+1;
3. 添加数组:把一个对象添加到数组中,数组内部会把这个对象的引用计数+1;
4. 属性赋值:会让对象的引用计数+1;
5. 属性关联:xib中的控件,跟代码关联后,会让对象的引用计数+1;
6. push这个操作会让对象的引用计数增加。
ARC、MRC混编
- ARC环境下 使用MRC代码,使用 -fno-objc-arc转换
- MRC环境下 使用ARC代码,使用 -fobjc-arc转换
所有权修饰符
- _ _strong 修饰符
- _ _weak 修饰符
- _ _unsafe_unretained 修饰符(MRC环境下使用)
- _ _autoreleasing 修饰符 (ARC环境下使用)
1、_ _strong修饰符是id类型和对象类型的默认修饰符,一般是隐式的;
id obj = [[NSObject alloc] init];
//同上
id _ _strong obj = [[NSObject alloc] init];
2、 _ _weak 解决 循环引用的 修饰符,防止内存泄漏。在持有某对象的弱引用时,若该对象被废弃,则弱引用自动失效,且被赋值nil,不在持有该对象(持有的对象超出作用域,会被废弃销毁)。
3、_ _unsafe_unretained 和 _ _weak修饰的变量一样,因为自己生成并持有的对象不能继续为自己所有,所以生成的对象会被立即释放。
4、_ _autoreleasing 隐式修饰符:非自己生成的对象,自己也能持有,会注册到自动释放池中。
属性声明的属性 与 所有权修饰符的对应关系
声明的属性 | 所有权修饰符 | 说明 |
---|---|---|
assign | _ _unsafe_unretained | 修饰基本数据类型,如NSInteger和CGFloat,这些数值主要存在于栈上。 |
copy | _ _strong | 每次复制会在内存中拷贝一份对象,指针指向不同的地址 |
retain | _ _strong | 用来持有对象 |
strong | _ _strong | 持有对象,引用计数会增加1。该对象只要引用计数不为0则不会被销毁。当然强行将其设为nil可以销毁它。 |
unsafe_unretained | _ _unsafe_unretained | 不安全持有 |
weak | _ _weak | 不拥有该对象。其修饰的对象引用计数不会增加。无需手动设置,该对象会自行在内存中销毁。 |
释放对象时,废弃对象不被持有的动作:
1、objc_release (释放)
2、因为引用计数为0,所以执行dealloc(释放内存)
3、_objc_rootDealloc(根解除)
4、object_dispose(废弃处理)
5、objc_destructInstance(销毁)
6、objc_clear_deallocating(清除)
对象被废弃时,最后调用objc_clear_deallocating的动作(相关表 SideTables
):
1、从weak表中获取废弃对象的记录(地址为键值)。address= h(key)
2、将包含在记录中的所有附有_ _weak修饰符变量的地址,赋值nil。
3、从weak表中删除该记录。
4、从引用计数表中删除废弃对象的记录(地址为键值)。
/* MRC环境
* NSAutoreleasePool
* autorelease
*/
- (void)test2 {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
//同上
@autoreleasepool {
id obj = [[NSObject alloc] init];
[obj autorelease];
}
//同上 (ARC:ReferenceCountingVC2 )
@autoreleasepool {
id __autoreleaseing obj2;
obj2 = obj
}
}
下面了解几个概念:
内存泄漏:
应当废弃的对象在超出其生存周期后继续存在。
野指针:
指针变量未初始化,其值是随机的;指针释放后未置空,指向不存在的内存。
悬垂指针:
指向曾经存在的对象,但是该对象已经不存在了。
标记指针:
Tagged Pointer
是一种特殊的“指针”,其特殊在于,其实它存储的并不是地址,而是真实的数据和一些附加的信息。(后面做专门讲解)
isa 指针:
NONPOINTER_ISA
对象的isa指针,用来表明对象所属的类类型和一些附加信息。(nonPointer_isa 后面做专门讲解)
标记指针: Tagged Pointer
tagged pointer是一种特殊的“指针”,其特殊在于,其实它存储的并不是地址,而是真实的数据和一些附加的信息。
可以看到,利用tagged pointer后,“指针”即存储了对象本身,也存储了和对象相关的标记。这时的tagged pointer里面存储的不是地址,而是一个数据集合。同时,其占用的内存空间也由16字节缩减为8字节。
(即:Tagged Pointer = 对象本身 + 对象相关标记)
我们可以在WWDC2013的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:
• Tagged Pointer专门用来存储小的对象,例如NSNumber
,NSDate
,NSString
。
• Tagged Pointer指针的值不再是地址了,而是真正的值。实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
• 在内存读取上有着3倍的效率,创建时比以前快10倍
例如:NSString
其输出的class类型为 NSTaggedPointerString
。在字符串长度在9个以内时,iOS其实使用了tagged pointer做了优化的。
直到字符串长度大于9,字符串才真正成为了__NSCFString
类型。
首先,iOS需要一个标志位来判断当前指针是真正的指针还是tagged pointer。
在runtime源码的objc-internal.h中,有关于标志位
的定义如下:
#if __has_feature(objc_fixed_enum) || __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2, // 标志位是2的 tagger pointer表示这是一个NSString对象。
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
OBJC_TAG_RESERVED_7 = 7,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
#if __has_feature(objc_fixed_enum) && !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif
‘地址’以
0xa
开头,转换为二进制是1010
, 首位1表示这是一个tagged pointer
,而010转换为十进制是2,表示这是一个NSString类型
。‘地址’以
0xb
开头, 转换为二进制是1011
, 首位1表示这是一个tagged pointer
, 而011转换为十进制是3,表示这是一个NSNumber类型
。
(即:标志位是2的tagger pointer表示这是一个NSString对象。)
由于一个tagged pointer所指向的并不是一个真正的OC对象,它其实是没有isa属性的。
isa 指针(NONPOINTER_ISA)
isa:
是一个指向对象所属Class类型的指针。(nonPointer_isa)
对象的isa指针,用来表明对象所属的类类型和一些附加信息。
如果isa指针仅表示类型的话,对内存显然也是一个极大的浪费。于是,就像tagged pointer一样,对于isa指针,苹果同样进行了优化
。isa指针表示的内容变得更为丰富,除了表明对象属于哪个类之外,还附加了 「引用计数」extra_rc
,是否有被weak引用标志位weakly_referenced
,是否有附加对象标志位has_assoc
等信息。(rc:reference counter 引用计数)
//------- NSObject(实质) -------
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
//------- objc_class(继承)-------
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable 以前的缓存指针和虚函数表
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags class_rw_t *加上自定义rr/alloc标志
//。。。
}
//------- isa指针(指向)-------
struct objc_object {
private:
isa_t isa;
public:
Class ISA(); // ISA() assumes this is NOT a tagged pointer object 假设这不是一个标记的指针对象
Class getIsa(); // getIsa() allows this to be a tagged pointer object 允许这是一个标记的指针对象
//。。。
}
//------- 联合体(定义)-------
union isa_t
{
isa_t() { } //构造函数1
isa_t(uintptr_t value) : bits(value) { } //构造函数2
Class cls; //成员1(占据64位内存空间)
uintptr_t bits; //成员2(占据64位内存空间)
#if SUPPORT_PACKED_ISA
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct { //成员3(占据64位内存空间:从低位到高位依次是nonpointer到extra_rc。成员后面的:表明了该成员占用几个bit。)
uintptr_t nonpointer : 1; //(低位)注意:标志位,表明isa_t *是否是一个真正的指针!!!
uintptr_t has_assoc : 1; // 关联对象
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1; //弱引用
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1; //引用计数 相关成员1
uintptr_t extra_rc : 19; //引用计数 相关成员2 (用19位来 记录对象的引用次数)
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
}
引用于书籍《Objective-C高级编程》
GNUstep 是Cocoa 框架的互换框架。
CF:Core Foundation
CFRutime.c
苹果的实现大概是采用散列表(引用计数表)来管理引用计数。
在runtime中,有四个数据结构非常重要,分别是SideTables
、SideTable
、weak_table_t
和weak_entry_t
。它们和对象的引用计数,以及weak引用相关。
(相关记录,我的电脑文件:101-Runtime基础)
引用计数表的关系
其实在绝大多数情况下,仅用优化的isa_t
来记录对象的引用计数
就足够了。只有在19位的extra_rc
盛放不了那么大的引用计数时,才会借助SideTable出马。
SideTable:是一个 全局的引用计数表,它记录了所有对象的引用计数。
先说一下这四个数据结构的关系。 在runtime内存空间中,SideTables 是一个长度为8个元素 的hash数组,里面存储了SideTable
。SideTables
的hash键值就是一个对象obj的address。即:table1 = h(address1)
因此可以说,一个obj,对应了一个SideTable
。但是一个SideTable
,会对应多个obj。因为SideTable
的数量只有64个,所以会有很多obj共用同一个SideTable
。
SideTables
SideTable地址 | 关键字 |
---|---|
table1 = h ( address1 ) | address1 |
table2 = h ( address2 ) | address1 |
table3 = h ( address3 ) | address1 |
... | ... |
SideTable
obj对象地址 | 关键字 |
---|---|
address1 = h ( key1 ) 、RefcountMap1、weak_table1 | key1 |
address2 = h ( key2 ) 、RefcountMap2、weak_table2 | key2 |
address3 = h ( key3 )、RefcountMap3、weak_table3 | key3 |
... | ... |
而在一个SideTable中,又有3个成员,分别是:
spinlock_t slock
: 自旋锁,用于上锁/解锁 SideTable。RefcountMap refcnts
:以DisguisedPtr<objc_object>为key的hash表,用来存储OC对象的引用计数(仅在未开启isa优化 或 在isa优化情况下isa_t的引用计数溢出时才会用到)。weak_table_t weak_table
: 存储对象弱引用指针的hash表。是OC weak功能实现的核心数据结构。
struct
weak_table_t
{
weak_entry_t *weak_entries
; // 保存了所有指向指定对象的 weak 指针
size_tnum_entries
; // 存储空间
uintptr_tmask
; // 参与判断引用计数辅助量
uintptr_tmax_hash_displacement
; // hash key 最大偏移值
};
其中,refcents
是一个hash map,其key是obj的地址,而value,则是obj对象的引用计数。 即:referenceCount = h ( address )
而weak_table
则存储了弱引用obj的指针的地址,其本质是一个以obj地址为key,弱引用obj的指针的地址作为value的hash表。hash表的节点类型是weak_entry_t
。 即:objPointer = h ( address )
二、Block
三、GCD与多线程
iOS全解1-3:锁、GCD与多线程
iOS全解1-4:NSURLSession
参考文章,如有问题请联系本人QQ1178690076
Object-C高级编程读书笔记(1)——Block的基本概念
Object-C高级编程读书笔记(2)——Block的实质
Object-C高级编程读书笔记(3)——Block的变量截取
Object-C高级编程读书笔记(4)——__block说明符
Object-C高级编程读书笔记(5)——Block的对象类型截取
Objective-C runtime机制(1)——基本数据结构:objc_object & objc_class
Objective-C runtime机制(2)——消息机制
Objective-C runtime机制(3)——method swizzling
Objective-C runtime机制(4)——深入理解Category
Objective-C runtime机制(5)——iOS 内存管理
Objective-C runtime机制(6)——weak引用的底层实现原理
Objective-C runtime机制(7)——SideTables, SideTable, weak_table, weak_entry_t
Objective-C runtime机制(8)——OC对象从创建到销毁
Objective-C runtime机制(9)——main函数前发生了什么
Objective-C runtime机制(10)——KVO的实现机制
Objective-C runtime机制(11)——结业考试