前言
我前面的博客探讨对象的本质与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
分析:CGM.CreateRuntimeFunction(FTy, "objc_setProperty")
,创建了objc_setProperty
方法。说明创建objc_setProperty方法llvm需要调用getSetPropertyFn()函数。源码查找调用getSetPropertyFn()函数的地方,向上查找最后定位到GetPropertySetFunction()
函数,即调用此函数就生成objc_setProperty方法。
分析:strategy.getkind()等于GetSetProperty
时会调用GetPropertySetFunction()方法。那么strategy.getkind()什么情况下等于GetSetProperty呢?
分析:当属性被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
)。SEL
和IMP
的关系就像是一本书的目录
与页码
,SEL是目录名称,IMP是页码,即SEL对应方法名,IMP对应方法实现的指针地址
。
总结:
实例方法
在类
中,类方法
在元类
中- 编译器
自动生成元类
,目的是存放类方法