Objc4-818底层探索(五):类信息内容补充

建议先看下Objc4-818底层探索(三):isa
建议先看下Objc4-818底层探索(四):isa与类

主要针对于类信息, 补充一些知识点:

知识点1:成员变量与属性

成员变量

@interface ViewController (){

    NSString *name ; // 成员变量, 实例变量
    int age;   // 成员变量, 基本数据类型变量
    id data;   // 成员变量, 实例变量
}
  • 通常在.h/.m文件以@interface{ } 形式定义的变量
成员变量的访问权限
@interface ViewController (){
    
    NSString * A; 
    
@public
    NSString * B;
    
@protected
    NSString * C;
    
@private
    NSString * D;
    
    @package
    NSString * E;
}
  • @public:在任何地方都能直接访问对象的成员变量

  • @private:只能在当前类的对象方法中直接访问, 如果子类要访问需要调用父类的get/set方法

  • @protected:可以在当前类及其子类对象方法中直接访问,变量默认的访问权限就是 protected

  • @package:只能在framework内部的类是@protected的权限,对于外部的类是@private,相当于框架级的保护权限,适合使用在静态库.a中。

实例变量与基础数据类型变量
  • 如果成员变量是一个类(类的实例化), 则这个变量实例变量, 例如上面例子name, data (id 是 OC特有的类型。从本质上讲, id 等同于 (void *))都是实例变量。而ageint型, 像int, double, short等是基础数据类型变量

  • 实例变量 + 基础数据类型变量 = 成员变量

属性(属性变量)

@property (nonatomic, strong) NSString *hobby;  //属性
  • 通常在.h/.m文件以@interface{ } 形式定义的变量

  • 编译器会自动为属性生成set, get方法, 以及生成成员变量_documentsDirectory即成员变量名前加下划线形式, 详细看下面的知识点2

  • 属性是用于与其他对象交互的变量, 正因为要与其他对象交互, 就有了属性修饰符或者叫属性特质, 如:nonatomic, readwrite, copy 等等



知识点2:关于成员变量和属性在底层

创建一个只有main的项目 (创建main项目可以参考我这一篇章: IOS创建个只有main.m工程) 。里面添加一个对象继承NSObject, 并添加一些的成员变量和属性。

@interface SATest : NSObject {
    NSString *insStr;
}

@property (atomic, copy) NSString *atoCopyStr;
@property (nonatomic, copy) NSString *nonCopyStr;
@property (atomic) NSString *atoStr;
@property (nonatomic) NSString *nonStr;

@end

clang一下, 看下前端编译器中底层实现

clang -rewrite-objc main.m -o main.cpp
clang
  • @property属性在底层取消了属性而是转换成"下划线+成员变量"以及set, get方法的形式

  • @ interface{}成员变量在底层还是以成员变量的存放, 不会有set, get方法

之前的探索我们还能得到以下结论

  • 通过@interface XXXX {}定义的成员变量,会存储在bits属性中,通过bits → data() → ro() → ivars获取成员变量列表,除了包括成员变量,还包括属性成员变量

  • 通过@property定义的属性,不仅仅在ro() → ivars中以下划线存在, 并且还存储在bits属性中,通过bits --> data() --> properties() --> list获取属性列表,其中只包含property属性



知识点3: 关于objc_setProperty

objc_setProperty

在clang底层中, 会发现属性有些set方法里面有一个objc_setProperty方法, 而且有些方法有, 有些没有, 我们接下来探索下objc_setProperty。818源码查找下objc_setProperty

objc818下objc_setProperty

objc818下objc_setProperty

可看到除了objc_setProperty以外还有objc_setProperty_atomic, objc_setProperty_nonatomic, objc_setProperty_atomic_copy, objc_setProperty_nonatomic_copy而且他们都直接调用一个reallySetProperty

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

可看到他在底层的操作主要是对新增的retain, 对旧值的release

reallySetProperty

objc_setProperty相当于一个承上启下接口, 上层许多个set方法直接对接llvm, 则llvm需要针对每一个set做对应处理, 则会很麻烦。所以苹果设置了一个中间层(接口隔离层)objc_setProperty, 令他处理一部分set方法, 保证无论上层怎么变, 传入llvm的格式不会有变化。主要是达到上下层接口隔离的目的。

objc_setProperty
LLVM中的objc_setProperty

看底层可看出, 有些需要有些需要objc_setProperty, 有些并无调用objc_setProperty, 这块要看底层LLVM, 看下LLVM怎么处理的objc_setProperty。这里我们要用逆推法

[第一步] 查询objc_setProperty

因为要查找objc_setProperty, 全局搜索objc_setProperty。发现getSetPropertyFn()方法中调用CGM.CreateRuntimeFunction(FTy, "objc_setProperty");CGM创建runtime函数objc_setProperty

  • Fn: 为function函数的缩写
objc_setProperty
[第二步] 查询getSetPropertyFn()

全局搜索什么方法调用getSetPropertyFn(), 可看到GetPropertySetFunction调用

getSetPropertyFn()
[第三步] 查询GetPropertySetFunction

全局搜索什么方法调用GetPropertySetFunction, 可看到如果case为GetSetPropertySetPropertyAndExpressionGet会调用GetPropertySetFunction

GetPropertySetFunction()

-其中UseOptimizedSetter(CGM)这个判断后面标注为// 10.8 and iOS 6.0 code and GC is off即10.8和iOS 6.0代码,GC关闭, 所以新版本直接走GetPropertySetFunction

[第四步] 查询GetSetProperty

全局搜索什么方法调用GetSetProperty, 可看见PropertyImplStrategy中有

GetSetProperty
GetSetProperty
  • IsCopy: 如果属性修饰符为copy, 直接会调用objc_setProperty

  • Retain & !IsAtomic: 如果属性修饰符为retain, 并且为非原子性nonatomic, 也会调用objc_setProperty

[第五步] 验证objc_setProperty
验证

可看到 copy或者 retain&nonatomic,会调用objc_setProperty



知识点4: 关于编码

编码

clang命令之后我们会发现会出现很多T, @, v...这些符号, 那这些又是什么呢?

iOS系统提供了一个叫做@encode指令,可以将具体的类型表示成字符串编码

  1. @encode实际上是编译器指令其中的一种。
  2. @encode能够返回一个Objective-C 类型编码(Objective-C Type Encodings)。
  3. @encode是一种编译器内部表示的字符串,方便识别,类似于 ANSI C 的 typeof 操作。

具体看下苹果官方定义:

苹果Type Encodings:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html

Type Encodings

苹果Property Type and Functions:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW1

Property Type and Functions

当然我们也可以通过命令查看编码

#pragma mark - 各种类型编码
void lgTypes(void){
    NSLog(@"char --> %s",@encode(char));
    NSLog(@"int --> %s",@encode(int));
    NSLog(@"short --> %s",@encode(short));
    NSLog(@"long --> %s",@encode(long));
    NSLog(@"long long --> %s",@encode(long long));
    NSLog(@"unsigned char --> %s",@encode(unsigned char));
    NSLog(@"unsigned int --> %s",@encode(unsigned int));
    NSLog(@"unsigned short --> %s",@encode(unsigned short));
    NSLog(@"unsigned long --> %s",@encode(unsigned long long));
    NSLog(@"float --> %s",@encode(float));
    NSLog(@"bool --> %s",@encode(bool));
    NSLog(@"void --> %s",@encode(void));
    NSLog(@"char * --> %s",@encode(char *));
    NSLog(@"id --> %s",@encode(id));
    NSLog(@"Class --> %s",@encode(Class));
    NSLog(@"SEL --> %s",@encode(SEL));
    int array[] = {1,2,3};
    NSLog(@"int[] --> %s",@encode(typeof(array)));
    typedef struct person{
        char *name;
        int age;
    }Person;
    NSLog(@"struct --> %s",@encode(Person));
    
    typedef union union_type{
        char *name;
        int a;
    }Union;
    NSLog(@"union --> %s",@encode(Union));

    int a = 2;
    int *b = {&a};
    NSLog(@"int[] --> %s",@encode(typeof(b)));
}
编码代码打印

知识点5: 关于类与isa走位图面试题

先看下isa走位图

isa走位图

熟悉走位图之后我们看几个题目

题目1:

看下这个例子返回打印结果(回复有或者没有即可)

//- (void)sayHello;
//+ (void)sayHappy;

void lgInstanceMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
    Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));
    Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
    Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
   
    NSLog(@"method1: %p", method1);
    NSLog(@"method2: %p", method2);
    NSLog(@"method3: %p", method3);
    NSLog(@"method4: %p", method4);
    
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        SATest *test = [SATest alloc];
        Class pClass = object_getClass(test);
    
        lgInstanceMethod_classToMetaclass(pClass);
        //lgClassMethod_classToMetaclass(pClass);
        //lgIMP_classToMetaclass(pClass);
        
    }
    return 0;
}
解题思路:

先看下class_getInstanceMethod源码

/***********************************************************************
* class_getInstanceMethod.  Return the instance method for the
* specified class and selector.
**********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    // This deliberately avoids +initialize because it historically did so.

    // This implementation is a bit weird because it's the only place that 
    // wants a Method instead of an IMP.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}

就是判断类是否有指定的实例方法, 实例方法存在当前类中, 类方法存在元类中

// - (void)sayHello; 实例方法
// + (void)sayHappy; 类方法

  • class_getInstanceMethod(pClass, @selector(sayHello));, 判断当前类是否有实例方法sayHello, 有
  • class_getInstanceMethod(metaClass, @selector(sayHello));, 判断元类是否有实例方法sayHello, 无
  • class_getInstanceMethod(pClass, @selector(sayHappy));, 判断当前类是否有类方法sayHappy, 无
  • class_getInstanceMethod(metaClass, @selector(sayHappy));, 判断元类是否有类方法sayHappy, 有
结果:
问题1结果


题目2:

void lgClassMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(sayHello));
    Method method2 = class_getClassMethod(metaClass, @selector(sayHello));
    Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
    Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
    
    NSLog(@"method1: %p", method1);
    NSLog(@"method2: %p", method2);
    NSLog(@"method3: %p", method3);
    NSLog(@"method4: %p", method4);
}
/***********************************************************************
* class_getClassMethod.  Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

    Class getMeta() {
        if (isMetaClassMaybeUnrealized()) return (Class)this;
        else return this->ISA();
    }

那么

  • class_getClassMethod(pClass, @selector(sayHello)), 在当前类中找实例方法sayHello, 无
  • class_getClassMethod(metaClass, @selector(sayHello)), 在元类中找实例方法sayHello, 无
  • class_getClassMethod(pClass, @selector(sayHappy)), 在当前类中找类方法sayHappy, 有
  • class_getClassMethod(metaClass, @selector(sayHappy)), 在元类中找类方法sayHappy, 因为传入的是元类, 这里cls->getMeta()返回元类本身, 所以有
结果:
问题2结果


题目3:

#import "SATest.h"

void lgKindofDemo(void){
    
    BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
    BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
    BOOL re3 = [(id)[SATest class] isKindOfClass:[SATest class]];
    BOOL re4 = [(id)[SATest class] isMemberOfClass:[SATest class]];
    NSLog(@"\n re1 :%hhd \n re2 :%hhd \n re3 :%hhd \n re4 :%hhd \n",re1,re2,re3,re4);

    BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
    BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
    BOOL re7 = [(id)[SATest alloc] isKindOfClass:[SATest class]];
    BOOL re8 = [(id)[SATest alloc] isMemberOfClass:[SATest class]];
    NSLog(@"\n re5 :%hhd \n re6 :%hhd \n re7 :%hhd \n re8 :%hhd \n",re5,re6,re7,re8);
}


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        lgKindofDemo();
        NSLog(@"Hello World");
    
    }
    return 0;
    
}

解题思路:

需要先看isKindOfClass, isMemberOfClass底层

isMemberOfClass

先看下isMemberOfClass底层实现

// 类方法
+ (BOOL)isMemberOfClass:(Class)cls {
    // 判断当前元类是否与传入类相等
    return self->ISA() == cls;
}

// 实例方法方法
- (BOOL)isMemberOfClass:(Class)cls {
    // 判断当前类是否与传入类相等
    return [self class] == cls;
}
// 类方法
+ (BOOL)isKindOfClass:(Class)cls {
    // 循环判断
    // 元类 vs 传入类
    // 父元类 vs 传入类
    // 根元类 vs 传入类
    // 根类 vs 传入类
    for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

// 实例方法
- (BOOL)isKindOfClass:(Class)cls {
    // 循环判断
    // 类 vs 传入类
    // 父类 vs 传入类
    // 根类 vs 传入类
    // nil vs 传入类
    for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}
  • [XXX class]: 取得类, 调用类方法
  • [XXX alloc]: 开辟对象, 调用对象/实例方法
  1. [(id)[NSObject class] isKindOfClass:[NSObject class]];: 类方法, 判断NSObject即与传入类NSObject。相等

2.[(id)[NSObject class] isMemberOfClass:[NSObject class]];: 类方法, 判断NSObject元类根元类与传入类NSObject。不相等

3.[(id)[SATest class] isKindOfClass:[SATest class]]: 类方法, 依次判断SATest父类, 根类, nil与传入类SATest。不相等

4.[(id)[SATest class] isMemberOfClass:[SATest class]];: 类方法, 判断SATest元类元类与传入类SATest。不相等

5.[(id)[NSObject alloc] isKindOfClass:[NSObject class]];: 实例方法, 循环判断NSObject与传入类NSObject。相等

6.[(id)[NSObject alloc] isMemberOfClass:[NSObject class]];: 实例方法, 判断NSObject与传入类NSObject。相等

7.[(id)[SATest alloc] isKindOfClass:[SATest class]];: 实例方法, 循环判断SATest与传入类SATest。相等

  1. [(id)[SATest alloc] isMemberOfClass:[SATest class]];: 实例方法, 判断SATest类与传入类SATest。相等

固结果为 1, 0, 0, 0, 1, 1, 1, 1

结果:

问题3结果

但实际上这么思考, 结果没问题但过程有问题, 因为我们打开终端, 会发现系统实际上没有调用 isKindOfClass, 而是调用objc_opt_isKindOfClass, 系统重定向了isKindOfClass方法。

objc_opt_isKindOfClass
objc_opt_isKindOfClass

那么我们需要查找objc_opt_isKindOfClass

// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
    if (slowpath(!obj)) return NO;

    // 做了一步obj->getIsa() 获取isa, 即
    // 如果obj 是对象,则isa是类,
    // 如果obj是类,则isa是元类
    Class cls = obj->getIsa();
    // 缓存中是否能查找到当前类的isKindOfClass方法
    // 找到走if 
    if (fastpath(!cls->hasCustomCore())) {
        // 循环判断
        // 如果是对象
        // 当前类 vs 传入类
        // 父类 vs 传入类
        // 根类 vs 传入类
        // nil vs 传入类

        // 如果是类
        // 元类 vs 传入类
        // 父元类 vs 传入类
        // 根元类 vs 传入类
        // 根类 vs 传入类
        for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
            if (tcls == otherClass) return YES;
        }
        return NO;
    }
#endif
     // 没找到走消息转发, 发送`isKindOfClass`
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}

// class or superclass has default new/self/class/respondsToSelector/isKindOfClass
// 判断当前类是否有个默认的 isKindOfClass
#define FAST_CACHE_HAS_DEFAULT_CORE   (1<<15)
    bool hasCustomCore() const {
        return !cache.getBit(FAST_CACHE_HAS_DEFAULT_CORE);
    }

所以回头再看下1, 3, 5, 7

  1. [(id)[NSObject class] isKindOfClass:[NSObject class]];: 判断NSObject即与传入类NSObject。相等

3.[(id)[SATest class] isKindOfClass:[SATest class]]: 循环判断SATest元类, 根元, 与传入类SATest。不相等

5.[(id)[NSObject alloc] isKindOfClass:[NSObject class]];: 判断NSObject即与传入类NSObject 。相等

7.[(id)[SATest alloc] isKindOfClass:[SATest class]];: 判断SATest类即与传入类SATest类。 相等



知识点6: armv7, arm64, i386 , x86_64解答

  • armv7 | armv7s | arm64是ARM处理器的指令集

  • i386 | x86_64是Mac 处理的指令集。

下面是指令集在设备的使用

arm64iPhone5S之后的iPhone系列, iPad Air以及iPad mini2 之后的iPad系统

armv7siPhone5iPhone5CiPad4(iPad with Retina Display)

armv7iPhone4iPhone4SiPadiPad2iPad3iPad miniiPod Touch 3GiPod Touch4



i386: 是针对intel通用的微处理器32位处理器

x86_64: 是针对x86架构64位处理器

  • 模拟器32位处理器测试要i386的架构

  • 模拟器64位处理器测试要x86_64的架构

  • 真机32位处理器要armv7 或者armv7s

  • 真机64位处理器要arm64架构

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容