Comment on page
03 知识点梳理:iOS底层
KVC
可以通过Key
直接访问对象的属性,可以方便的在运行时对对象的属性进行访问和修改。需要注意的是: 有时我们通过KVC
去修改系统控件未暴露出的属性的时候,会出现部分内部API进行了调整导致崩溃。
1.1 API
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
forKeyPath
用于访问对象内对象属性的属性,如:user.name
1.2 写过程
- 查看
setKey:
方法是否存在, 如果存在直接调用, 如果不存在进入下一步 - 查看
_setKey:
方法是否存在, 如果存在直接调用, 如果不存在进入下一步 - 查看
+ (BOOL)accessInstanceVariablesDirectly
方法的返回值, 默认返回YES
YES
: 可以访问成员变量, 进入下一步NO
: 不可以访问成员变量, 同时调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key
方法, 如果方法不存在会抛出异常
- 调用成员变量:
_key
,_isKey
,key
,isKey
- 调用顺序, 从左到右, 只有发现存在成员变量, 就不会在调用后续变量
- 如果没有成员变量, 会调用
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
方法, 如果方法不存在会抛出异常
1.3 读过程
- 判断是否有这几个方法:
getKey
,key
,isKey
,_key
- 从左到右, 如果有方法, 直接调用, 取值结束
- 如果没有进入下一步
- 调用+ (BOOL)accessInstanceVariablesDirectly查看是否可以访问成员变量. 默认
YES
YES
: 可以访问成员变量, 进入下一步NO
: 不可以访问成员变量, 判断是否实现- (id)valueForUndefinedKey:(NSString *)key
方法, 实现时调用, 未实现报错
- 判断是否有这几个成员变量:
_key
,_isKey
,key
,isKey
- 从左到右, 如果有成员变量, 直接访问, 取值结束
- 如果没有这几个成员变量, 直接进入下一步
- 判断是否实现
- (id)valueForUndefinedKey:(NSString *)key
方法, 实现时调用, 未实现报错
1.4 如何禁用KVC
如果重写了类方法
+ (BOOL)accessInstanceVariablesDirectly
返回 NO
的话,那么会直接调用 valueForUndefinedKey:
方法,默认是抛出异常A: atomic的实现机制
atomic
是property
的修饰词之一,表示是原子性的,使用方式为@property(atomic)int age
;此时编译器会自动生成getter/setter
方法,最终会调用objc_getProperty
和objc_setProperty
方法来 进行存取属性。- 若此时属性用
atomic
修饰的话,在这两个方法内部使用os_unfair_lock
来进行加锁,来保证读写的原子性。锁都在PropertyLocks
中保存着(在iOS平台会初始化8个,mac平台64个),在用之前,会把锁都初始化好,在需要用到时,用对象的地址加上成员变量的偏移量为key
,去PropertyLocks
中去取。因此存取时用的是同一个锁,所以atomic能保证属性的存取时是线程安全的。 - 注:由于锁是有限的,不用对象,不同属性的读取用的也可能是同一个锁
B: atomic为什么不能保证绝对的线程安全?
atomic
在getter/setter
方法中加锁,仅保证了存取时的线程安全,假设我们的属性是@property(atomic)NSMutableArray *array
;可变的容器时,无法保证对容器的修改是线程安全的.- 在编译器自动生产的
getter/setter
方法,最终会调用objc_getProperty
和objc_setProperty
方法存取属性,在此方法内部保证了读写时的线程安全的,当我们重写getter/setter
方法时,就只能依靠自己在getter/setter
中保证线程安全
Autoreleasepool
是由多个AutoreleasePoolPage
以双向链表的形式连接起来的.Autoreleasepool
的基本原理:在每个自动释放池创建的时候,会在当前的AutoreleasePoolPage
中设置一个标记位,在此期间,当有对象调用autorelsease
时,会把对象添加AutoreleasePoolPage
中- 若当前页添加满了,会初始化一个新页,然后用双向量表链接起来,并把新初始化的这一页设置为
hotPage
,当自动释放池pop时,从最下面依次往上pop,调用每个对象的release
方法,直到遇到标志位。
AutoreleasePoolPage
结构如下class AutoreleasePoolPage {
magic_t const magic;
id *next;//下一个存放autorelease对象的地址
pthread_t const thread; //AutoreleasePoolPage 所在的线程
AutoreleasePoolPage * const parent;//父节点
AutoreleasePoolPage *child;//子节点
uint32_t const depth;//深度,也可以理解为当前page在链表中的位置
uint32_t hiwat;
}
4.1 什么是内省?
在计算机科学中,内省是指计算机程序在运行时(Run time)检查对象(Object)类型的一种能力,通常也可以称作运行时类型检查。 不应该将内省和反射混淆。相对于内省,反射更进一步,是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。
4.2 iOS中内省的几个方法?
isMemberOfClass
//对象是否是某个类型的对象isKindOfClass
//对象是否是某个类型或某个类型子类的对象isSubclassOfClass
//某个类对象是否是另一个类型的子类isAncestorOfObject
//某个类对象是否是另一个类型的父类respondsToSelector
//是否能响应某个方法conformsToProtocol
//是否遵循某个协议*
4.3
class
方法和object_getClass
方法有什么区别?- 实例
class
方法就直接返回object_getClass(self)
- 类
class
方法直接返回self
,而object_getClass
(类对象),则返回的是元类
- 分类
- 主要用来为某个类添加方法,属性,协议(我一般用来为系统的类扩展方法或者把某个复杂的类的按照功能拆到不同的文件里)
- 扩展
- 主要用来为某个类原来没有的成员变量、属性、方法。注:方法只是声明(我一般用扩展来声明私有属性,或者把.h的只读属性重写成可读写的)
分类和扩展的区别:
- 分类是在运行时把分类信息合并到类信息中,而扩展是在编译时,就把信息合并到类中的
- 分类声明的属性,只会生成
getter/setter
方法的声明,不会自动生成成员变量和getter/setter
方法的实现,而扩展会 - 分类不可用为类添加实例变量,而扩展可以
- 分类可以为类添加方法的实现,而扩展只能声明方法,而不能实现
分类的局限性:
- 分类的方法若和类中原本的实现重名,会覆盖原本方法的实现,注:并不是真正的覆盖
- 多个分类的方法重名,会调用最后编译的那个分类的实现
分类的结构体里有哪些成员
struct category_t {
const char *name; //名字
classref_t cls; //类的引用
struct method_list_t *instanceMethods;//实例方法列表
struct method_list_t *classMethods;//类方法列表
struct protocol_list_t *protocols;//协议列表
struct property_list_t *instanceProperties;//实例属性列表
// 此属性不一定真正的存在
struct property_list_t *_classProperties;//类属性列表
};
Dealloc 的实现机制是内容管理部分的重点,把这个知识点弄明白,对于全方位的理解内存管理的只是很有 必要。
1.Dealloc 调用流程
- 1.首先调用
_objc_rootDealloc()
- 2.接下来调用
rootDealloc()
- 3.这时候会判断是否可以被释放,判断的依据主要有 5 个,判断是否有以上五种情况
NONPointer_ISA
weakly_reference
has_assoc
has_cxx_dtor
has_sidetable_rc
- 4-1.如果有以上五中任意一种,将会调用
object_dispose()
方法,做下一步的处理。 - 4-2.如果没有之前五种情况的任意一种,则可以执行释放操作,
C 函数的 free()
。 - 5.执行完毕。
2.object_dispose() 调用流程。
- 1.直接调用
objc_destructInstance()
。 - 2.之后调用 C 函数的 free()。
3.
objc_destructInstance()
调用流程- 1.先判断
hasCxxDtor
,如果有 C++ 的相关内容,要调用object_cxxDestruct()
,销毁 C++ 相关的内容。 - 2.再判断
hasAssocitatedObjects
,如果有的话,要调用object_remove_associations()
, 销毁关联对象的一系列操作。 - 3.然后调用
clearDeallocating()
。 - 4.执行完毕。
4.
clearDeallocating()
调用流程。- 1.先执行
sideTable_clearDellocating()
。 - 2.再执行
weak_clear_no_lock
,在这一步骤中,会将指向该对象的弱引用指针置为nil
。 - 3.接下来执行
table.refcnts.eraser()
,从引用计数表中擦除该对象的引用计数。 - 4.至此为止,
Dealloc
的执行流程结束。
- 静态链接是指将多个目标文件合并为一个可执行文件,直观感觉就是将所有目标文件的段合并。需要注意的是可执行文件与目标文件的结构基本一致,不同的是是否“可执行”。
- 静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
- 动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。
- 优化DNS解析和缓存
- 对传输的数据进行压缩,减少传输的数据
- 使用缓存手段减少请求的发起次数
- 使用策略来减少请求的发起次数,比如在上一个请求未着地之前,不进行新的请求
- 避免网络抖动,提供重发机制
@implementation Son : Father
- (id)init
{
self = [super init];
if (self)
{
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
self和super的区别:
self
是类的一个隐藏参数,每个方法的实现的第一个参数即为self
。- super并不是隐藏参数,它实际上只是一个**”编译器标示符”**,它负责告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。
在调用
[super class]
的时候,runtime
会去调用objc_msgSendSuper
方法,而不是objc_msgSend
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
}
在
objc_msgSendSuper
方法中,第一个参数是一个objc_super
的结构体,这个结构体里面有两个变量,一个是接收消息的receiver
,一个是当前类的父类super_class
。入院考试第一题错误的原因就在这里,误认为
[super class]
是调用的[super_class class]
。objc_msgSendSuper
的工作原理应该是这样的:- 从
objc_super
结构体指向的superClass
父类的方法列表开始查找selector, - 找到后以
objc->receiver
去调用父类的这个selector
。注意,最后的调用者是objc->receiver
,而不是super_class
!
那么
objc_msgSendSuper
最后就转变成objc_msgSend(objc_super->receiver, @selector(class))
+ (Class)class {
return self;
}
下面代码输出什么?
@interface Sark : NSObject
@end
@implementation Sark
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
NSLog(@"%d %d %d %d", res1, res2, res3, res4);
}
return 0;
}
先来分析一下源码这两个函数的对象实现
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
inline Class
objc_object::getIsa()
{
if (isTaggedPointer()) {
uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
return ISA();
}
inline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
return (Class)(isa.bits & ISA_MASK);
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
首先题目中NSObject 和 Sark分别调用了class方 法。
+ (BOOL)isKindOfClass:(Class)cls
方法内部,会先去获得object_getClass
的类,而object_getClass
的源码实现是去调用当前类的obj->getIsa()
,最后在ISA()
方法中获得meta class
的指针。- 接着在
isKindOfClass
中有一个循环,先判断class
是否等于meta class
,不等就继续循环判断是否等于super class
,不等再继续取super class
,如此循环下去。

[NSObject class]
执行完之后调用isKindOfClass
,第一次判断先判断NSObject
和NSObject
的meta class
是否相等,之前讲到meta class
的时候放了一张很详细的图,从图上我们也可以看出,NSObject
的meta class
与本身不等。- 接着第二次循环判断
NSObject
与meta class
的superclass
是否相等。还是从那张图上面我们可以看到:Root class(meta)
的superclass
就是Root class(class)
,也就是NSObject
本身。所以第二次循环相等,于是第一行res1
输出应该为YES
。 - 同理,
[Sark class]
执行完之后调用isKindOfClass
,第一次for
循环,Sark
的Meta Class
与[Sark class]
不等,第二次for循环
,Sark
Meta Class的super class
指向的是NSObject Meta Class
, 和Sark Class
不相等。 - 第三次for循环,
NSObject Meta Class
的super class
指向的是NSObject Class
,和Sark Class
不相等。第四次循环,NSObject Class
的super class
指向nil
, 和Sark Class
不相等。第四次循环之后,退出循环,所以第三行的res3输出为NO
。 - 如果把这里的Sark改成它的实例对象,
[sark isKindOfClass:[Sark class]
,那么此时就应该输出YES
了。因为在isKindOfClass
函数中,判断sark的meta class
是自己的元类Sark
,第一次for循环就能输出YES
了。 isMemberOfClass
的源码实现是拿到自己的isa指针
和自己比较,是否相等。- 第二行
isa
指向NSObject
的Meta Class
,所以和NSObject Class
不相等。第四行,isa
指向Sark
的Meta Class
,和Sark Class
也不等,所以第二行res2
和第四行res4
都输出NO。
下面的代码会?
Compile Error / Runtime Crash / NSLog…?
@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end
@implementation Sark
- (void)speak {
NSLog(@"my name's %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Sark class];
void *obj = &cls;
[(__bridge id)obj speak];
}
@end
这道题有两个难点。
- 难点一:
obj
调用speak
方法,到底会不会崩溃。 - 难点二:如果
speak
方法不崩溃,应该输出什么?
首先需要谈谈隐藏参数self和_cmd的问题。 当
[receiver message]
调用方法时,系统会在运行时偷偷地动态传入两个隐藏参数self
和_cmd
,之所以称它们为隐藏参数,是因为在源代码中没有声明和定义这两个参数。self
在已经明白了,接下来就来说说_cmd
。_cmd
表示当前调用方法,其实它就是一个方法选择器SEL
。- 难点一,能不能调用
speak
方法?
id cls = [Sark class];
void *obj = &cls;
答案是可以的。
obj
被转换成了一个指向Sark Class
的指针,然后使用id
转换成了objc_object
类型。obj
现在已经是一个Sark
类型的实例对象了。当然接下来可以调用speak的方法。- 难点二,如果能调用
speak
,会输出什么呢?
很多人可能会认为会输出sark相关的信息。这样答案就错误了。
正确的答案会输出
my name is <ViewController: 0x7ff6d9f31c50>
内存地址每次运行都不同,但是前面一定是ViewController。why?
我们把代码改变一下,打印更多的信息出来。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"ViewController = %@ , 地址 = %p", self, &self);
id cls = [Sark class];
NSLog(@"Sark class = %@ 地址 = %p", cls, &cls);
void *obj = &cls;
NSLog(@"Void *obj = %@ 地址 = %p", obj,&obj);
[(__bridge id)obj speak];
Sark *sark = [[Sark alloc]init];
NSLog(@"Sark instance = %@ 地址 = %p",sark,&sark);
[sark speak];
}
我们把对象的指针地址都打印出来。输出结果:
ViewController = <ViewController: 0x7fb570e2ad00> , 地址 = 0x7fff543f5aa8
Sark class = Sark 地址 = 0x7fff543f5a88
Void *obj = <Sark: 0x7fff543f5a88> 地址 = 0x7fff543f5a80
my name is <ViewController: 0x7fb570e2ad00>
Sark instance = <Sark: 0x7fb570d20b10> 地址 = 0x7fff543f5a78
my name is (null)
objc_msgSendSuper2 解读
// objc_msgSendSuper2() takes the current search class, not its superclass.
OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);
objc_msgSendSuper2方法入参是一个objc_super *super。
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
};
#endif
所以按viewDidLoad执行时各个变量入栈顺序从高到底为
self
, _cmd
, self.class
, self
, obj
。- 第一个
self
和第二个_cmd
是隐藏参数。 - 第三个
self.class
和第四个self
是[super viewDidLoad]
方法执行时候的参数。 - 在调用
self.name
的时候,本质上是self
指针在内存向高位地址偏移一个指针。在32位下面,一个指针是4字节=4*8bit=32bit
。(64位不一样但是思路是一样的) - 从打印结果我们可以看到,
obj
就是cls
的地址。在obj
向上偏移32bit
就到了0x7fff543f5aa8
,这正好是ViewController
的地址。
所以输出为my name is
<ViewController: 0x7fb570e2ad00>