ios 类的结构分析下

前言

我前面的博客探讨对象的本质与isa我们发现成员变量底层实现只是添加了一个变量而没有实现get和set方法,而属性变量底层实现会变成_变量,并且会实现get和set方法,但是底层set方法有些是通过objc_setProperty设置属性值,有些是通过首地址+偏移量的方式,当时我们留了一个悬念。同样在博客类的结构分析上中我们发现类的实例方法在类中,但是类的类方法在哪里呢?今天就深入探讨下这两个问题。

1.0 objc_setProperty

我们用对象的本质与isa中的示例main.cpp接着分析。

main.m如下:

@interface LGWPerson:NSObject{
    NSString* sex;
}
@property(nonatomic,copy)NSString* name;
@property(nonatomic,strong)NSString* nickname;
@property(nonatomic,assign)int age;
@end
@implementation LGWPerson
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}
复制代码

main.cpp如下:

typedef struct objc_object LGWPerson;
typedef struct {} _objc_exc_LGWPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LGWPerson$_name;
extern "C" unsigned long OBJC_IVAR_$_LGWPerson$_nickname;
extern "C" unsigned long OBJC_IVAR_$_LGWPerson$_age;
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
typedef struct objc_class *Class;

struct NSObject_IMPL {
  Class isa;
};

struct LGWPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *sex;
	int _age;
	NSString *_name;
	NSString *_nickname;
};


// @implementation LGWPerson

static NSString * _I_LGWPerson_name(LGWPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGWPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_LGWPerson_setName_(LGWPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGWPerson, _name), (id)name, 0, 1); }

static NSString * _I_LGWPerson_nickname(LGWPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGWPerson$_nickname)); }
static void _I_LGWPerson_setNickname_(LGWPerson * self, SEL _cmd, NSString *nickname) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGWPerson$_nickname)) = nickname; }

static int _I_LGWPerson_age(LGWPerson * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_LGWPerson$_age)); }
static void _I_LGWPerson_setAge_(LGWPerson * self, SEL _cmd, int age) { (*(int *)((char *)self + OBJC_IVAR_$_LGWPerson$_age)) = age; }
// @end

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
    }
    return 0;
}
复制代码

分析:属性变量name的set方法是通过objc_setProperty设置值的,而属性nickname和age的set方法是通过首地址+偏移量设置值的。说明代码编译的时候就已经确定了使用哪种方式设置属性的值。

1.0.1 LLVM探索set_Property

既然编译的时候就确定了属性变量set方法,那就在LLVM源码中探索一下set_Property

7871624877600_.pic_hd.jpg
分析:CGM.CreateRuntimeFunction(FTy, "objc_setProperty"),创建了objc_setProperty方法。说明创建objc_setProperty方法llvm需要调用getSetPropertyFn()函数。源码查找调用getSetPropertyFn()函数的地方,向上查找最后定位到GetPropertySetFunction()函数,即调用此函数就生成objc_setProperty方法。

7891624879147_.pic_hd.jpg
分析:strategy.getkind()等于GetSetProperty时会调用GetPropertySetFunction()方法。那么strategy.getkind()什么情况下等于GetSetProperty呢?
WeChate21c1e3151282ff48e433aa759c2000f.png
分析:当属性被copy修饰是kind=GetSetProperty,也就是说属性被copy修饰时,设置属性的值才会调用set_Property,否则设置属性的值为首地址+偏移量。

1.0.2 objc源码探索objc_setProperty

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) __attribute__((always_inline));

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);
    }
    //新值retain
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    //release旧值
    objc_release(oldValue);
}
复制代码

分析:objc_setProperty设置属性值的过程就是release旧值,retain新值

总结:

  • copy修饰的属性设置新值时使用objc_setProperty方式实现,其它属性使用首地址+偏移量设置新值。
  • copy一般用来修饰不可变对象,如NSString。属于浅拷贝,只是对指针的拷贝不会分配新的内存
  • strong修饰的对象,属于深拷贝会分配新的内存
  • assign修饰对象会造成野指针,因为assign设置新值时不会release旧值

2.0 类方法归属分析

对象实例元类实例,那么对象的实例方法中,那么类方法是否在元类中呢?我们通过runtime探索一下

@interface GyPerson : NSObject
- (void)sayHello;
+ (void)sayHappy;
@end
@implementation GyPerson
- (void)sayHello{
    
}
+ (void)sayHappy{
    
}
@end

void lgObjc_copyMethodList(Class pClass){
    unsigned int count = 0;
    Method *methods = class_copyMethodList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        //获取方法名
        NSString *key = NSStringFromSelector(method_getName(method));
        
        LGLog(@"Method, name: %@", key);
    }
    free(methods);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        const char * className = class_getName(GyPerson.class);
        
        Class gyperson=objc_getClass(className);
        lgObjc_copyMethodList(gyperson);
        
        Class meteperson=objc_getMetaClass(className);
        lgObjc_copyMethodList(meteperson);
    }
   return 0;
}

复制代码

输出如下:

Method, name: sayHello
Method, name: sayHappy
复制代码

分析:通过runtime api class_copyMethodList获取到类和元类的方法列表,并打印出来,发现sayHello()实例方法在GyPerson类中,sayHappy()类方法在GyPerson元类中,这正好验证了我们的猜想。我们还可以利用runtime api class_getInstanceMethod来判断是否存在sayHello()或sayHappy()方法。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        const char * className = class_getName(GyPerson.class);
        //类
        Class gyperson=objc_getClass(className);
        //元类
        Class meteperson=objc_getMetaClass(className);
        //判断类中是否有sayHello
        Method method1=class_getInstanceMethod(gyperson, @selector(sayHello));
        //判断类中是否有sayHappy
        Method method2=class_getInstanceMethod(gyperson, @selector(sayHappy));
        //判断元类中是否有sayHello
        Method method3=class_getInstanceMethod(meteperson, @selector(sayHello));
        //判断元类中是否有sayHappy
        Method method4=class_getInstanceMethod(meteperson, @selector(sayHappy));
        NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    }
   return 0;
}
复制代码

输出如下:

0x100008110-0x0-0x0-0x1000080a8
复制代码

分析:0x100008110是方法sayHello()指针地址,0x1000080a8是方法sayHappy()指针地址,0x0代表没有。

我们通过源码看一下Method的结构


struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

复制代码

分析:Method是一个指向objc_method结构体指针,有三个属性变量method_name(SEL)、method_types(char*)、method_imp(IMP)。SELIMP的关系就像是一本书的目录页码,SEL是目录名称,IMP是页码,即SEL对应方法名,IMP对应方法实现的指针地址

总结:

  • 实例方法中,类方法元类
  • 编译器自动生成元类,目的是存放类方法
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享