Objective-C对象本质
Objective-C的代码和对象底层是怎样实现的
- Objective-C代码,底层都是C/C++实现的
- Objective-C对象是基于C/C++的结构体实现的
将Objective-C 代码转换为C++代码
#import <Foundation/Foundation.h>
int main(int argc, char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
return 0;
}
}
转换之后
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
return 0;
}
}
一个OC对象占用对少内存?
系统分配了16个字节,但是NSObject对象只占用了8个字节(64位环境下)
NSLog(@"%zd",class_getInstanceSize([NSObject class]));//打印结果为8
NSObject *obj = [[NSObject alloc] init];
NSLog(@"%zd",malloc_size((__bridge const void *)obj));//打印结果为16,在源码中判断size小于16时 会将它重新赋值为16
一个自定义Person对象占多少内存?
- Person对象含有两个实例变量
@interface Person :NSObject{
@public
int _age;
int _height;
}
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
p->_age = 20;
p->_height = 100;
NSLog(@"%zd",class_getInstanceSize([Person class]));//结果为16
NSLog(@"%zd",malloc_size((__bridge const void *)(p)));//结果为16
}
return 0;
}
- Person对象含三个实例变量
@interface Person :NSObject{
@public
int _age;
int _height;
int _wight;
}
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
p->_age = 20;
p->_height = 100;
p->_wight = 100;
NSLog(@"%zd",class_getInstanceSize([Person class]));//结果为24
NSLog(@"%zd",malloc_size((__bridge const void *)(p)));//结果为32
}
return 0;
}
两个容易混淆的函数
- 创建一个实例对象,至少需要多少内存?
#import <objc/runtime.h>
class_getInstanceSize([NSObject class])
- 创建一个实例对象实际上分配多少内存
#import <malloc/malloc.h>
malloc_size((__bridge const void *)(obj))
Objective-C 中的对象 主要分哪几种?
1、instance对象(实例对象)
- 就是通过类alloc出来的对象,每次alloc都会生成新的实例对象
- 实例对象在内存中存储的信息
- 保存了isa
- 成员变量(变量的值)
2、 class对象(类对象)
NSObject *obj = [[NSObject alloc] init];
Class objectClass = [obj class];
Class objectClass2 = [NSObject class];
Class objectClass3 = object_getClass(obj);
//objectClass objectClass2 objectClass3 都是类对象 且都是同一个对象
NSLog(@"%p %p %p",objectClass,objectClass2,objectClass3);
//打印结果为 0x7fffa9e05140 0x7fffa9e05140 0x7fffa9e05140
- 一个类的类对象是唯一的,在内存中只有一份
- 类对象在内存中存储的信息
- isa指针
- super_class指针
- 类的属性信息(@property)
- 类的对象方法信息(instance method)(- 开头的方法 不包括 + 开头的)
- 类的协议信息(protocol)
- 类的成员变量信息(ivar)(成员变量的类型 和名字等描述信息, 不包括值,值是由实例对象保存的)
3、meta-class对象(元类对象)
Class objectMetaClass = object_getClass([NSObject class]);//元类对象
NSLog(@"%p",objectMetaClass);
Class objectClass4 = [[NSObject class] class];//返回的不是元类对象,而是类对象,class方法不管调用多少次 返回的都是类对象
- 每个类也只有一个元类对象
- 元类对象存储的信息
- isa指针
- super_class指针
- 类方法(+开头的方法)
对象的isa指针指向哪里
- 实例对象的isa指针指向的是它对应的类对象(对象调用的对象方法保存在类对象中)
- 类对象的isa指针指向的事它对应的元类对象(类对象调用的类方法保存在元类对象中)
- 64位的系统 通过isa得到的地址值要经过一次位运算才能得到真正的地址(superClass指针不存在这个问题)
类对象的superClass指针
- 类对象的superClass指针指向父类的类对象
元类对象的superClass指针
- 元类对象的superClass指针指向父类的元类对象
关系总结
- instance的isa指向class
- class的isa指向meta-class
- mete-class的isa指向基类的mate-class
- class的superClass指向父类的class
- 如果没有父类,superClass指针为nil(只针对类对象)
- meta-class的superClass指向父类的meta-class
- 基类的meta-class的superClass指向基类的class
- instance调用对象方法调用轨迹
- 通过实例对象的isa指针找到类对象,寻找对应的对象方法
- 如果没有对应的方法,就通过类对象的superClass找到父类的类对象,寻找对应的对象方法
- 如果还是没找到,就再通过superClass指针一直往上找
- 如果一直没有,最终会找到基类,如果再没有就找不到了,编译器就会报错
KVO(Key-Value-Observing)
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向全新的子类
- 当修改
instance
对象的属性时,会调用Foundation
的_NSSetXXXValueAndNotify
函数:willChangeValueForKey:
- 父类原来的
setter
didChangeValueForKey:
- 内部会触发监听器(
Observe
)的监听方法(observeValueForKeyPath:ofObject:change:context:
)
新的类对象会重写属性的
set
方法Class
方法dealloc
方法 和_isKVOA
方法
如何手动触发KVO
手动调用willChangeValueForKey:
和didChangeValueForKey:
直接修改成员变量会触发KVO吗?
不会,只有调用set方法的时候才会触发,因为KVO本质是利用重写set方法来生效的
KVC(Key-Value-Coding)
setValue:forKey: 和setValue:forKeyPath的区别
setValue:forKeyPath: 可以访问多层属性 例如:
Person *per = [[Person alloc] init];
per.cat = [[Cat alloc] init];
[per setValue:@10 forKey:@"age"];
NSLog(@"%d",per.age);//10
[per setValue:@20 forKeyPath:@"cat.weight"];
NSLog(@"%d",per.cat.weight);//20
通过KVC修改属性 会触发KVO吗?
会触发KVO,哪怕没有对应的key的属性(也就不会自动生成set方法),也没有手动实现属性的set方法,依然会触发KVO(可能是KVC内部针对kVO做了处理,执行了willChangeValueForKey
和didChangeValueForKey
)。
KVC的赋值和取值过程是怎样的?原理是什么?
赋值过程
setValue:forKey:
执行流程:
- 按照
setKey:
_setKey:
的顺序查找方法,如果能找到就传递参数,调用方法 - 如果上面两个方法都没有找到,会调用
+(BOOL)accessInstanceVariablesDirectly
方法,查看这个方法的返回值 - 如果返回NO,会抛出异常
NSUnknownKeyException
- 如果返回YES,会按照
_key
_isKey
key
isKey
的顺序查找成员变量,能找到的话就直接赋值,找不到的话,也会抛出异常NSUnknownKeyException
取值过程
valueForKey:
执行流程
- 按照
getKey
key
isKey
_key
的顺序找到方法,如果能找到就调用方法,取值 - 如果找不到上面的方法会调用
+(BOOL)accessInstanceVariablesDirectly
方法,查看这个方法的返回值 - 如果返回NO,会调用
valueForUndefinedKey:
方法并 抛出异常NSUnknownKeyException
- 如果返回YES,会按照
_key
_isKey
key
isKey
的顺序查找成员变量,能找到的话就直接取值,找不到的话,也会抛出异常NSUnknownKeyException
Category
Category的实现原理
- Category编译之后的底层结构是struct category_t 结构体,里面存储着分类的对象方法,类方法,属性,协议信息
- 在程序运行的时候,runtime会将Category的数据合并到类(类对象、元类对象)信息中
注:
- Category中的方法如果与原来的方法相同时,Category中的方法会被优先调用,原来的方法并没有被覆盖, 只是方法被找到后就不会继续查找
- 如果两个Category中都有相同的方法,那么在Complle Sources 中考后的分类(会被最后编译)中的方法会被优先调用
Category和Class Extension的区别是什么
- Extension 在编译的时候,它的数据就已经包含在类信息中
- Category是在运行时,才会将数据合并到类信息中
- 无法为系统的类添加Extension,除非创建它的子类,但可以给系统的类添加Category
Category中有load方法吗?load方法是什么时候调用的?load方法能否继承?
- 分类中有load方法
- load方法在runtime加载类、分类的时候调用,只调用一次
- 调用顺序
- 先调用类的+load(按照编译先后顺序调用,先编译先调用,调用子类的+load方法之前会先调用父类的+load)
- 再调用分类的+load(按照编译先后顺序调用,先编译先调用)
- load方法严格来说是存在继承的(比如子类中没有实现load方法,会调用父类的load方法,但这个问题需要显式的调用[Student load]才会验证,此时走的是消息发送机制,子类未实现某个方法时会去父类中查找),但一般情况下不会主动去调用load方法,都是让系统自动调用
+initialize方法
-
+initialize
方法会在类第一次接收到消息时调用 - 子类的
+initialize
方法调用之前会先调用父类的+initialize
(如果父类之前没有初始化),先初始化父类,再初始化子类,每个类只会初始化一次(子类的+initialize
可能不会被调用)
load、initialize方法的区别是什么?它们在Category中的调用顺序是什么?出现继承时它们之间的调用过程是什么?
区别
- 调用方式的区别
-
+load
是根据函数地址直接调用 -
+initialize
是通过objc_msgSend
调用
-
- 调用时刻的区别
-
+load
是runtime
加载类、分类的时候调用(只会调用一次) -
+initialize
是类第一次接收到消息的时候调用,每一个类只会+initialize
一次,但是父类的+initialize
方法可能会被调用多次(子类没有实现+initialize
方法的时候)
-
调用顺序
- load
- 先调用类的
+load
,先编译的类优先调用+load
方法,调用子类的+load
之前,会先调用父类的+load
- 再调用分类的
+load
,先编译的分类,优先调用+load
- 先调用类的
- initialize
- 先初始化父类
- 再初始化子类(可能最终调用的是父类的
+initialize
方法)
Category能否添加成员变量?如果可以,如何添加?
- 不能直接给Category添加成员变量,可以间接实现分类有成员变量的效果(通过关联对象)
如何实现给分类添加成员变量
默认情况下,因为分类底层结构的限制(分类底层结构体中并没有存储实例对象的数组),不能添加成员变量到分类中。但可以通过关联对象来间接实现
关联对象提供了以下API
- 添加关联对象
void objc_setAssociatedObject(id object, const void * key,
id value, objc_AssociationPolicy policy)
- 获得关联对象
id objc_getAssociatedObject(id object, const void * key)
- 移除所有关联对象
void objc_removeAssociatedObjects(id object)
关联对象的原理
- 关联对象并不是存储在被关联对象本身内存中
- 关联对象存储在全局的统一的一个
AssociationsManager
中 - 设置关联对象为nil,相当于移除关联对象
实现关联对象技术的核心对象
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- objcAssociation
关联对象方法中key的常见用法
static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
static char MyKey;
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, &MyKey)
//使用属性名作为key
objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");
//使用get方法的@selecor作为key
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))
网友评论