欢迎阅读iOS探索系列(按序阅读食用效果更加)c++
本文涉及的面试题以下:程序员
runtime
是由C
和C++
、汇编
实现的一套API,为OC语言加入了面向对象、运行时的功能运行时(runtime)
将数据类型的肯定由编译时推迟到了运行时runtime
是Objective-C
的幕后⼯做者如类结构中的ro
和rw
属性面试
ro(read-only)
在编译时已经肯定rw(read-write)
在运行时才肯定,所以可使用runtime
进行修改方法的本质是发送消息objc_msgSend
,即寻找IMP
的过程数组
发送消息会有如下⼏个流程:缓存
objc_msgSend
查找缓存cache_t
是否有imp实现lookUpImpOrForward
递归查找当前类和父类的rw
中methodlist
的方法resolveInstanceMethod
和resolveClassMethod
来动态方法决议——实现消息动态处理CoreFoundation
来触发消息转发流程,forwardingTargetForSelector
实现快速转发,由其余对象来实现处理方法methodSignatureForSelector
获取到方法的签名,生成对应的invocation
;再经过forwardInvocation
来进行处理遇到这种问题先要解释二者分别是什么?再解释二者的关系sass
SEL
是方法编号,也是方法名,在dyld加载镜像到内存时,经过_read_image
方法加载到内存的表中了bash
IMP
是函数实现指针,找IMP
就是找函数实现的过程架构
SEL
和IMP
的关系就能够解释为:app
SEL
就至关于书本的⽬录标题IMP
就是书本的⻚码函数
就是具体页码对应的内容好比咱们想在《程序员的自我修养——连接、装载与库》一书中找到“动态连接”(SEL)
,确定会翻到179页(IMP)
,179页会开始讲述具体内容(函数实现)
ide
不能。
ro
,而ro
是在编译时就已经肯定了的rw
中的方法或者能够经过关联对象的方式来添加属性这题对runtime-API要求程度比较高
/** *建立类 * *superClass: 父类,传Nil会建立一个新的根类 *name: 类名 *extraBytes: 额外的内存空间,通常传0 *return:返回新类,建立失败返回Nil,若是类名已经存在,则建立失败 */
Class FXPerson = objc_allocateClassPair([NSObject class], "LGPerson", 0);
复制代码
/** *添加成员变量 *这个函数只能在objc_allocateClassPair和objc_registerClassPair以前调用。不支持向现有类添加一个实例变量 *这个类不能是元类,不支持在元类中添加一个实例变量 *实例变量的最小对齐为1 << align,实例变量的最小对齐依赖于ivar的类型和机器架构。对于任何指针类型的变量,请经过log2(sizeof(pointer_type)) * *cls 往哪一个类添加 *name 添加的名字 *size 大小 *alignment 对齐处理方式 *types 签名 */
class_addIvar(FXPerson, "fxName", sizeof(NSString *), log2(sizeof(NSString *)), "@");
复制代码
/** *往内存注册类 * * cls 要注册的类 */
objc_registerClassPair(FXPerson);
复制代码
/** *往类里面添加属性 * *cls 要添加属性的类 *name 属性名字 *attributes 属性的属性数组。 *attriCount 属性中属性的数量。 */
class_addProperty(targetClass, propertyName, attrs, 4);
复制代码
/** *往类里面添加方法 * *cls 要添加方法的类 *sel 方法编号 *imp 函数实现指针 *types 签名 */
class_addMethod(FXPerson, @selector(setHobby), (IMP)fxSetter, "v@:@");
复制代码
// hobby的setter-IMP
void fxSetter(NSString *value) {
printf("%s/n",__func__);
}
// hobby的getter-IMP
NSString *fxHobby() {
return @"iOS";
}
// 添加属性变量的封装方法
void fx_class_addProperty(Class targetClass, const char *propertyName) {
objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
objc_property_attribute_t ownership = { "N", "" }; //N = nonatomic
objc_property_attribute_t backingivar = { "V", [NSString stringWithFormat:@"_%@",[NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding]].UTF8String }; //variable name
objc_property_attribute_t attrs[] = {type, ownership0, ownership, backingivar};
class_addProperty(targetClass, propertyName, attrs, 4);
}
// 打印属性变量的封装方法
void fx_printerProperty(Class targetClass){
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(targetClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 动态建立类
Class FXPerson = objc_allocateClassPair([NSObject class], "FXPerson", 0);
// 添加成员变量
class_addIvar(FXPerson, "name", sizeof(NSString *), log2(sizeof(NSString *)), "@");
// 注册到内存
objc_registerClassPair(FXPerson);
// 添加属性变量
fx_class_addProperty(FXPerson, "hobby");
fx_printerProperty(FXPerson);
// 添加方法(为属性方法添加setter、getter方法)
class_addMethod(FXPerson, @selector(setHobby:), (IMP)fxSetter, "v@:@");
class_addMethod(FXPerson, @selector(hobby), (IMP)fxHobby, "@@:");
// 开始使用
id person = [FXPerson alloc];
[person setValue:@"Felix" forKey:@"name"];
NSLog(@"FXPerson的名字是:%@ 爱好是:%@", [person valueForKey:@"name"], [person valueForKey:@"hobby"]);
}
return 0;
}
复制代码
<objc/runtime.h>
class_addIvar
必须在objc_registerClassPair
前,由于注册到内存时ro
已经肯定了,不能再往ivars
添加(同第四个面试题)class_addProperty
能够在注册内存先后,由于是往rw
中添加的class_addProperty
中“属性的属性”——nonatomic/copy
是根据属性的类型变化而变化的class_addProperty
不会自动生成setter和getter
方法,所以直接调用KVC
会崩溃不仅能够经过KVC
打印来检验,也能够下断点查看ro、rw
的结构来检验
实则是为了解决分类建立属性的问题
Category
在runtime
中是用一个结构体表示的:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
...
};
复制代码
里面虽然能够添加属性变量,可是这些properties
并不会自动生成Ivar
,也就是不会有 @synthesize
的做用,dyld加载期间,这些分类会被加载并patch到相应的类中。这是一个动态过程,Ivar
不能动态添加
手动实现setter、getter方法,关联对象
- (void)setName:(NSString *)name {
/** 参数一:id object : 给哪一个对象添加属性,这里要给本身添加属性,用self。 参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中经过次key得到属性的值并返回。 参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。 参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。 */
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name {
/** 参数一:id object : 获取哪一个对象里面的关联的属性。 参数二:void * == id key : 什么属性,与objc_setAssociatedObject中的key相对应,即经过key值取出value。 */
return objc_getAssociatedObject(self, @"name");
}
复制代码
objc_setAssociatedObject
分析苹果设计接口时每每会加个中间层——即便底层实现逻辑发生变化也不会影响到对外接口
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
复制代码
跟进去看看_object_set_associative_reference
实现
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;
assert(object);
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
// retain the new value (if any) outside the lock.
// 在锁以外保留新值(若是有)。
ObjcAssociation old_association(0, nil);
// acquireValue会对retain和copy进行操做,
id new_value = value ? acquireValue(value, policy) : nil;
{
// 关联对象的管理类
AssociationsManager manager;
// 获取关联的 HashMap -> 存储当前关联对象
AssociationsHashMap &associations(manager.associations());
// 对当前的对象的地址作按位去反操做 - 就是 HashMap 的key (哈希函数)
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
// 获取 AssociationsHashMap 的迭代器 - (对象的) 进行遍历
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
// 根据key去获取关联属性的迭代器
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
// 替换设置新值
j->second = ObjcAssociation(policy, new_value);
} else {
// 到最后了 - 直接设置新值
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
// 若是AssociationsHashMap从没有对象的关联信息表,
// 那么就建立一个map并经过传入的key把value存进去
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
// 若是传入的value是nil,而且以前使用相同的key存储过关联对象,
// 那么就把这个关联的value移除(这也是为何传入nil对象可以把对象的关联value移除)
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
// 最后把以前使用传入的这个key存储的关联的value释放(OBJC_ASSOCIATION_SETTER_RETAIN策略存储的)
if (old_association.hasValue()) ReleaseValue()(old_association);
}
复制代码
ObjcAssociation old_association(0, nil)
处理传进来的值获得new_value
AssociationsManager
,而后拿到hashmap总表AssociationsHashMap
DISGUISE(object)
对关联对象的地址进行取反操做获得哈希表对应的下标index
new_value
为空(即对属性赋值为nil)就直接找到相应的表进行删除new_value
不为空,就拿到总表的迭代器经过拿到的下标index进行遍历查找;若是找到管理对象的关联属性哈希map表,而后再经过key去遍历取值
仍是不明白就LLVM断点调试呗
objc_getAssociatedObject
分析id objc_getAssociatedObject(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
复制代码
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
// 关联对象的管理类
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 生成假装地址。处理参数 object 地址
disguised_ptr_t disguised_object = DISGUISE(object);
// 全部对象的额迭代器
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
// 内部对象的迭代器
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// 找到 - 把值和策略读取出来
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
// OBJC_ASSOCIATION_GETTER_RETAIN - 就会持有一下
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
复制代码
objc_getAssociatedObject
是objc_setAssociatedObject
的逆过程
当面试官问你weak置空原理是什么,你可能只知道weak怎么用殊不知道怎么答吧
在weak
一行打下断点运行项目
int main(int argc, const char * argv[]) {
@autoreleasepool {
FXPerson *person = [[FXPerson alloc] init];
id __weak person = object;
}
return 0;
}
复制代码
Xcode菜单栏Debug->Debug Workflow->Always show Disassembly
打上勾查看汇编——汇编代码会来到libobjc库
的objc_initWeak
①objc_initWeak
location
:表示__weak指针
的地址(咱们研究的就是__weak指针
指向的内容怎么置为nil)newObj
:所引用的对象,即例子中的person
id objc_initWeak(id *location, id newObj) {
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
复制代码
②storeWeak
HaveOld
:weak指针以前是否已经指向了一个弱引用HaveNew
:weak指针是否须要指向一个新引用CrashIfDeallocating
:若是被弱引用的对象正在析构,此时再弱引用该对象,是否应该crashstoreWeak最主要的两个逻辑点(源码太长,这里不贴了)
因为是第一次调用,因此走
haveNew
分支——获取到的是新的散列表SideTable,主要执行了weak_register_no_lock方法来进行插入
③weak_register_no_lock
isTaggedPointer
和deallocating
条件判断weak_table
中的weak_entry_t
哈希数组中取出对应的weak_entry_t
weak_entry_t
不存在,则会新建一个并插入referrer
经过函数append_referrer
插入到对应的weak_entry_t
引用数组④append_referrer
找到弱引用对象的对应的weak_entry
哈希数组中插入
因为弱引用在析构dealloc时自动置空,因此查看dealloc的底层实现并LLVM调试
_objc_rootDealloc
->rootDealloc
rootDealloc
->object_dispose
object_dispose
->objc_destructInstance
objc_destructInstance
->clearDeallocating
clearDeallocating
->sidetable_clearDeallocating
sidetable_clearDeallocating3
->table.refcnts.erase(it)
(非本人作图)具体可查阅iOS底层学习 - 内存管理之weak原理探究
对
method swizzing
不了解的能够阅读iOS逆向 代码注入+Hook
在平常开发中,再好的程序员都会犯错,好比数组越界
NSArray *array = @[@"F", @"e", @"l", @"i", @"x"];
NSLog(@"%@", array[5]);
NSLog(@"%@", [array objectAtIndex:5]);
复制代码
所以为了不数组越界这种问题,大神们开始玩起了黑魔法——method swizzing
NSArray分类
runtime
头文件——<objc/runtime.h>
+load
利用黑魔法交换方法#import "NSArray+FX.h"
#import <objc/runtime.h>
@implementation NSArray (FX)
+ (void)load {
// 交换objectAtIndex方法
Method oriMethod1 = class_getInstanceMethod(self, @selector(objectAtIndex:));
Method swiMethod1 = class_getInstanceMethod(self, @selector(fx_objectAtIndex:));
method_exchangeImplementations(oriMethod1, swiMethod1);
// 交换取下标方法
Method oriMethod2 = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:));
Method swiMethod2 = class_getInstanceMethod(self, @selector(fx_objectAtIndexedSubscript:));
method_exchangeImplementations(oriMethod2, swiMethod2);
}
- (void)fx_objectAtIndex:(NSInteger)index {
if (index > self.count - 1) {
NSLog(@"objectAtIndex————————数组越界");
return;
}
return [self fx_objectAtIndex:index];
}
- (void)fx_objectAtIndexedSubscript:(NSInteger)index {
if (index > self.count - 1) {
NSLog(@"取下标————————数组越界");
return;
}
return [self fx_objectAtIndexedSubscript:index];
}
@end
复制代码
然而程序仍是无情的崩了...
NSNumber、NSArray、NSDictionary
等这些类都是
类簇(Class Clusters)
,一个
NSArray
的实现可能由多个类组成。因此若是想对
NSArray
进行方法交换,必须获取到其真身进行方法交换,直接对NSArray进行操做是无效的
如下是NSArray
和NSDictionary
本类的类名
这样就好办了,可使用runtime
取出本类
黑魔法最好写成单例,避免屡次交换
好比说添加了[NSArray load]
代码,方法实现又交换回去了致使了崩溃
将+load
方法改写成单例(虽然不常见,但也要避免)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 交换objectAtIndex方法
Method oriMethod1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method swiMethod1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fx_objectAtIndex:));
method_exchangeImplementations(oriMethod1, swiMethod1);
// 交换取下标方法
Method oriMethod2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
Method swiMethod2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fx_objectAtIndexedSubscript:));
method_exchangeImplementations(oriMethod2, swiMethod2);
});
}
复制代码
①子类交换父类实现的方法
FXPerson类
中有-doInstance
方法,子类FXSon类
没有重写FXSon类
新建分类作了方法交换,新方法中调用旧方法FXPerson类
、FXSon类
调用-doInstance
子类打印出结果,而父类调用却崩溃了,为何会这样呢?
由于FXSon类
交换方法时取得doInstance
先在本类搜索方法,再往父类里查找,在FXFather
中找到了方法实现就把它跟新方法进行交换了。但是新方法是在FXSon分类
中的,FXFather
找不到imp就unrecognized selector sent to instance 0x600002334250
因此这种状况下应该只交换子类的方法,不动父类的方法
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod(self, @selector(doInstance));
Method swiMethod = class_getInstanceMethod(self, @selector(fx_doInstance));
BOOL didAddMethod = class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(self, @selector(fx_doInstance), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
复制代码
class_addMethod
给FXSon类
添加方法(class_addMethod
不会取代本类中已存在的实现,只会覆盖本类中继承父类的方法实现)
swiMethod
的实现和方法类型@selector(fx_doInstance)
添加方法class_addMethod
把新方法实现放到旧方法名中,此刻调用doInstance就是调用fx_doInstance
,可是调用fx_doInstance
会崩溃didAddMethod
判断是否添加成功
class_replaceMethod
替换方法method_exchangeImplementations
交换方法class_replaceMethod
用doInstance
方法实现替换掉fx_doInstance
中的方法实现②FXPerson类
只写了方法声明,没有方法实现,却作了方法交换——会形成死循环
doInstance
方法中添加了新的方法实现fx_doInstance
方法中想用旧的方法实现替代以前的方法实现,但是找不到doInstance
实现,致使class_replaceMethod
无效->在fx_doInstance
中调用fx_doInstance
就会死循环所以改变代码逻辑以下
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod(self, @selector(doInstance));
Method swiMethod = class_getInstanceMethod(self, @selector(fx_doInstance));
if (!oriMethod) {
class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd) {
NSLog(@"方法未实现");
}));
}
BOOL didAddMethod = class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(self, @selector(fx_doInstance), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
复制代码
doInstance
就是调用fx_doInstance
fx_doInstance
方法内部仍是调用本身,用block修改fx_doInstance
的实现,就能够断开死循环了,method_exchangeImplementations
交换失败使用Method Swizzling有如下注意事项:
+load
方法中交换方法单例
保证只交换一次这是一份作好封装的Method Swizzling
交换方法
+ (void)FXMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) {
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd) {
NSLog(@"方法未实现");
}));
}
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
}
复制代码
如今iOS面试都喜欢问些底层的问题,这能够很是直观的看出你对runtime的理解,并且在知识点上继续推敲、挖坑,当你答不上来时只能任人宰割——压低薪资或不录用。因此仍是要在平时多加练习,只要懂了原理就能触类旁通了,即使面试的时候不能打的十全十美,也能给面试官留下个好印象