美文网首页
iOS底层知识之OC语法

iOS底层知识之OC语法

作者: 左左4143 | 来源:发表于2018-08-20 19:03 被阅读196次

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指针指向父类的元类对象

关系总结

image
  • 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调用对象方法调用轨迹
    1. 通过实例对象的isa指针找到类对象,寻找对应的对象方法
    2. 如果没有对应的方法,就通过类对象的superClass找到父类的类对象,寻找对应的对象方法
    3. 如果还是没找到,就再通过superClass指针一直往上找
    4. 如果一直没有,最终会找到基类,如果再没有就找不到了,编译器就会报错

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做了处理,执行了willChangeValueForKeydidChangeValueForKey)。

KVC的赋值和取值过程是怎样的?原理是什么?

赋值过程

setValue:forKey: 执行流程:

  1. 按照setKey: _setKey:的顺序查找方法,如果能找到就传递参数,调用方法
  2. 如果上面两个方法都没有找到,会调用+(BOOL)accessInstanceVariablesDirectly方法,查看这个方法的返回值
  3. 如果返回NO,会抛出异常NSUnknownKeyException
  4. 如果返回YES,会按照_key _isKey key isKey 的顺序查找成员变量,能找到的话就直接赋值,找不到的话,也会抛出异常NSUnknownKeyException
取值过程

valueForKey:执行流程

  1. 按照getKey key isKey _key 的顺序找到方法,如果能找到就调用方法,取值
  2. 如果找不到上面的方法会调用+(BOOL)accessInstanceVariablesDirectly方法,查看这个方法的返回值
  3. 如果返回NO,会调用valueForUndefinedKey:方法并 抛出异常NSUnknownKeyException
  4. 如果返回YES,会按照_key _isKey key isKey 的顺序查找成员变量,能找到的话就直接取值,找不到的话,也会抛出异常NSUnknownKeyException

Category

Category的实现原理

  1. Category编译之后的底层结构是struct category_t 结构体,里面存储着分类的对象方法,类方法,属性,协议信息
  2. 在程序运行的时候,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中的调用顺序是什么?出现继承时它们之间的调用过程是什么?

区别
  1. 调用方式的区别
    • +load是根据函数地址直接调用
    • +initialize是通过objc_msgSend调用
  2. 调用时刻的区别
    • +loadruntime加载类、分类的时候调用(只会调用一次)
    • +initialize是类第一次接收到消息的时候调用,每一个类只会+initialize一次,但是父类的+initialize方法可能会被调用多次(子类没有实现+initialize方法的时候)
调用顺序
  1. load
    • 先调用类的 +load,先编译的类优先调用+load方法,调用子类的+load之前,会先调用父类的+load
    • 再调用分类的+load,先编译的分类,优先调用+load
  2. 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))

相关文章

网友评论

      本文标题:iOS底层知识之OC语法

      本文链接:https://www.haomeiwen.com/subject/ewzliftx.html