runtime实战

Objective-C语言是一门动态语言,很多事情都是在程序运行的时候处理的,今天主要和大家分享的不是理论上的东西。毕竟runtime已经是Objective-C老生常谈的东西了。如果你还不了解runtime是什么,这儿有一个runtime系列教程,讲的非常详细,大家可以先行阅读。
今天和大家分享的是一些非常经典的应用案例,毕竟学以致用才是最重要的。


1、作为基类(Model)实现NSCoding、NSCopy协议

这样,我们就不用为每个Model类写那些序列化或拷贝的协议了,这样有助于提高开发效率以及减少程序安装包的大小。

- (id)copyWithZone:(NSZone *)zone
{
    MYModel *model = [[[self class] allocWithZone:zone] init];
    Class fromClass = [self class];    
    while (fromClass != [MYModel class]) {
        NSUInteger count = 0;
        //读取类中的所有成员变量
        Ivar *iVarList = class_copyIvarList([fromClass class], &count);
        for (NSInteger i=0; i<count; ++i) {
            Ivar iVar = iVarList[i];//读取成员变量
            NSString *key = [NSString stringWithUTF8String:ivar_getName(iVar)];//获取成员变量的名称
            if (nil != key) {
                id value = [self valueForKey:key]; //获取成员变量的值
                if ([value isKindOfClass:[MYModel class]]) {
                    value = [value copy];
                }else if ([value isKindOfClass:[NSArray class]]
                          || [value isKindOfClass:[NSDictionary class]]
                          || [value isKindOfClass:[NSSet class]]
                          || [value isKindOfClass:[NSMutableString class]]){
                    if ([value conformsToProtocol:@protocol(NSMutableCopying)]) {
                        value = [value mutableCopy]; //优先使用深度拷贝
                    }else{
                        value = [value copy];
                    }
                }
                [model setValue:value forKey:key];//通过kvc来赋值
            }
        }
        free(iVarList); //记得要释放之前遍历结果,避免内存泄漏
        fromClass = class_getSuperclass(fromClass); //再遍历父类
    }
    return model;
}
//序列化

- (void)encodeWithCoder:(NSCoder *)encoder {
    Class cls = [self class];
    while (cls != [NSObject class]) {
        NSUInteger count = 0;
        Ivar* ivars = class_copyIvarList(cls, &count);
        for(NSInteger i=0; i<count; ++i)
        {
            Ivar iVar = iVarList[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(iVar)];
            if (key == nil){
                continue;
            }
            id value = [self valueForKey:key];
            if (value) {
                [encoder encodeObject:value forKey:key];
                }
            }
        }
        if (ivars) {
            free(ivars);
        }
        cls = class_getSuperclass(cls);
    }
}

//反序列化

- (id)initWithCoder:(NSCoder *)decoder {
    self = [super init];
    if (self) {
        Class cls = [self class];
        while (cls != [NSObject class]) {
            NSUInteger count = 0;
          Ivar* ivars = class_copyIvarList(cls, &count);
          for(NSInteger i=0; i<count; ++i)
          {
               Ivar iVar = iVarList[i];
                NSString *key = [NSString stringWithUTF8String:ivar_getName(iVar)];
                if (key == nil){
                    continue;
                }
                id value = [decoder decodeObjectForKey:key];
                if (value) {
                    [self setValue:value forKey:key];
                }
            }
            if (ivars) {
                free(ivars);
            }
            cls = class_getSuperclass(cls);
        }
    }
    return self;
}

总结:网上的例子中还有对结构体类型的变量的序列化方法,请参考对象自动序列化,但是这里要注意的是由于使用了kvc来做变量的读取,所以序列化和反序列化的耗时会有所增加,具体地化后面我们可以做一个验证。


2、hook开发

其中最常见的是swizzleMethod

@implementation NSObject (MethodSwizzle)

- (void)MYDealloc
{
    NSLog(@"一些crash的关键信息");  //这里你把写一些自己想要添加的方法
    [self MYDealloc];  //这里调用的是NSObject原来的dealloc,这里我们就能在对象释放之前做一些自己想要处理的事情
}

+ (BOOL)swizzleMethod:(SEL)origSel withMethod:(SEL)altSel
{
    Method originMethod = class_getInstanceMethod(self, origSel);
    Method newMethod = class_getInstanceMethod(self, altSel);

    if (originMethod && newMethod) {
        if (class_addMethod(self, origSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
            class_replaceMethod(self, altSel, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
        } else {
            method_exchangeImplementations(originMethod, newMethod);
        }
        return YES;
    }
    return NO;
}

+ (void)switchAllMethod
{
    //我们在所有的scrollview dealloc方面里面打印一句
    [UIScrollView swizzleMethod:@selector(dealloc) withMethod:@selector(MYDealloc)];
}

总结:MethodSwizzle的应用场景一般是在不改变现有类封装(或者系统方法)的前提下,实现一些我们想要达到的额外效果。
同时,它还有一个比较好的应用场景,就是做为内存闪照(MemorySnapshot),添加日志信息,辅助定位疑难crash。


3、打印description信息

-(NSString *) description
{
    NSMutableString *props = [NSMutableString string];
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList([self class], &outCount);
    for (i = 0; i<outCount; i++)
    {
        objc_property_t property = properties[i];
        const char* char_f =property_getName(property);
        NSString *propertyName = [NSString stringWithUTF8String:char_f];
        id propertyValue = [self valueForKey:(NSString *)propertyName];
        if (propertyValue){
            NSString *property = [NSString stringWithFormat:@"%@:%@\n",propertyName,propertyValue];
            [props appendString : property];
        }
    }
    free(properties);
    return props;
}

下面有一个更加详细的写法

- (NSString *)propertiesDescription
{
    NSMutableString *descriptionString = [[NSMutableString alloc] init];
    unsigned int propertyCount;
    objc_property_t *properties = class_copyPropertyList([self class], &propertyCount);

    for (unsigned int i = 0; i < propertyCount; i++)
    {
        NSString *selector = [NSString stringWithCString:property_getName(properties[i]) encoding:NSUTF8StringEncoding] ;
        SEL sel = sel_registerName([selector UTF8String]);

        const char *rawAttributeString = property_getAttributes(properties[i]);
        NSString* attributeString = [NSString stringWithUTF8String:rawAttributeString?rawAttributeString:""];
        NSArray* attributeArray = [attributeString componentsSeparatedByString:@","];
        NSString* attributeType = ([attributeArray count]>0)? [attributeArray objectAtIndex:0] : @"";
        NSString* propertyType = [attributeType substringFromIndex:1];
        const char * rawPropertyType = [propertyType UTF8String];
        Method mt = class_getInstanceMethod([self class], sel);
        NSString *formatString;
        NSString *resultString = @"";
        if (strcmp(rawPropertyType, @encode(char)) == 0) {
            formatString = @"%s : %zd\r ";
             char charValue = ((char(*)(id, Method))method_invoke)(self, mt);
            resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), charValue];

        } else if (strcmp(rawPropertyType, @encode(unsigned char)) == 0) {
            formatString = @"%s : %tu\r ";
            unsigned char charValue = ((unsigned char(*)(id, Method))method_invoke)(self, mt);
            resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), charValue];

        } else if (strcmp(rawPropertyType, @encode(short)) == 0) {
            formatString = @"%s : %zd\r ";
            short intValue = ((short(*)(id, Method))method_invoke)(self, mt);
            resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), intValue];

        } else if (strcmp(rawPropertyType, @encode(unsigned short)) == 0) {
            formatString = @"%s : %tu\r ";
            unsigned short intValue = ((unsigned short(*)(id, Method))method_invoke)(self, mt);
            resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), intValue];

        } else if (strcmp(rawPropertyType, @encode(int)) == 0) {
            formatString = @"%s : %zd\r ";
            int intValue = ((int(*)(id, Method))method_invoke)(self, mt);
            resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), intValue];

        } else if (strcmp(rawPropertyType, @encode(unsigned int)) == 0) {
            formatString = @"%s : %tu\r ";
            unsigned int intValue = ((unsigned int(*)(id, Method))method_invoke)(self, mt);
            resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), intValue];

        } else if (strcmp(rawPropertyType, @encode(long)) == 0) {
            formatString = @"%s : %ld\r ";
            long longValue = ((long(*)(id, Method))method_invoke)(self, mt);
            resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), longValue];

        } else if (strcmp(rawPropertyType, @encode(unsigned long)) == 0) {
            formatString = @"%s : %lu\r ";
            unsigned long longValue = ((unsigned long(*)(id, Method))method_invoke)(self, mt);
            resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), longValue];

        } else if (strcmp(rawPropertyType, @encode(long long)) == 0) {
            formatString = @"%s : %lld\r ";
            long long longValue = ((long long(*)(id, Method))method_invoke)(self, mt);
            resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), longValue];

        } else if (strcmp(rawPropertyType, @encode(unsigned long long)) == 0) {
            formatString = @"%s : %llu\r ";
            unsigned long long longValue = ((unsigned long long(*)(id, Method))method_invoke)(self, mt);
            resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), longValue];

        } else if (strcmp(rawPropertyType, @encode(float)) == 0) {
            formatString = @"%s : %f\r ";
            float floatValue = ((float(*)(id, Method))method_invoke)(self, mt);
            resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), floatValue];

        } else if (strcmp(rawPropertyType, @encode(double)) == 0) {
            formatString = @"%s : %f\r ";
            double doubleValue = ((double(*)(id, Method))method_invoke)(self, mt);
            resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), doubleValue];

        } else if ([propertyType hasPrefix:@"@"]) {
            formatString = @"%s : %@\r ";
            @try {
                id value = ((id(*)(id, Method))method_invoke)(self, mt);
                resultString = [NSString stringWithFormat:formatString, property_getName(properties[i]), value];
            }
            @catch (NSException *exception) {
                resultString = [NSString stringWithFormat:@"Encode Return Value Type  in %@", NSStringFromClass([self class])];
            }
            @finally {
            }
        } else {
            continue;
        }
        [descriptionString appendString:resultString];
    }    
    free(properties);
    [descriptionString insertString:@"{\r " atIndex:0];
    [descriptionString appendString:@"}\r"];
    return descriptionString;
}

总结:使用规范的@encode类型判断比直接使用objc_msgSend()方法更好,不过显然第一种写法更加简洁,通过kvc获取到的值会把int,short这些数据转成NSNumber。


4、关联对象

objc_setAssociatedObject(tableView, @"favReuseCache", reuseCache, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSCache* reuseCache = objc_getAssociatedObject(tableView, @"favReuseCache");

总结:关联的对象并不需要我们手动去进行释放,因为其是存放在另一个hashTalbe中的,当我们的对象进行释放时,关联对象也会被释放。另外一个OBJC_ASSOCIATION_RETAIN_NONATOMIC属性的合理选择,要避免提前释放造成的野指针。


5、使用NSInvocation

//NSInvocation调用
SEL mySelector = @selector(stringForDate:usingFormatter:);
NSMethodSignature * sig = [[currentDateClassObject class] 
instanceMethodSignatureForSelector: mySelector];

NSInvocation * myInvocation = [NSInvocation invocationWithMethodSignature: sig];
[myInvocation setTarget: currentDateClassObject];
[myInvocation setSelector: mySelector];

NSDate * myDate = [NSDate date];
[myInvocation setArgument: &myDate atIndex: 2];

NSDateFormatter * dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateStyle: NSDateFormatterMediumStyle]; 
[myInvocation setArgument: &dateFormatter atIndex: 3];

NSString * result = nil; 
[myInvocation retainArguments]; 
[myInvocation invoke];
[myInvocation getReturnValue: &result];
NSLog(@"The result is: %@", result);

总结:这里的参数为什么要从2开始呢,因为前面两个参数已经固定为id self与 SEL cmd了,如果还不清楚,请查阅IMP的使用例子。


6、Lua

既然说到runtime的应用那就有必要聊一聊Lua脚本在iOS中的应用,其实Lua正是利用了runtime的特性,让我们来看一段Lua的转换代码

        klass = objc_allocateClassPair(superClass, className, 0);
        NSUInteger size;
        NSUInteger alignment;
        NSGetSizeAndAlignment("*", &size, &alignment);
        class_addIvar(klass, WAX_CLASS_INSTANCE_USERDATA_IVAR_NAME, size, alignment, "*"); // Holds a reference to the lua userdata
        objc_registerClassPair(klass);        

        // Make Key-Value complient
        class_addMethod(klass, @selector(setValue:forUndefinedKey:), (IMP)setValueForUndefinedKey, "v@:@@");
        class_addMethod(klass, @selector(valueForUndefinedKey:), (IMP)valueForUndefinedKey, "@@:@");        

        id metaclass = object_getClass(klass);

        // So objects created in ObjC will get an associated lua object
        // Store the original allocWithZone implementation in case something secret goes on in there. 
        // Calls to `alloc` always are end up calling `allocWithZone:` so we don't bother handling alloc here.
        Method m = class_getInstanceMethod(metaclass, @selector(allocWithZone:));

        // If we the method has already been swizzled (by the class's super, then
        // just leave it up to the super!
        if (method_getImplementation(m) != (IMP)allocWithZone) {
            class_addMethod(metaclass, @selector(wax_originalAllocWithZone:), method_getImplementation(m), method_getTypeEncoding(m));
            class_addMethod(metaclass, @selector(allocWithZone:), (IMP)allocWithZone, "@@:^{_NSZone=}");
        }

总结:从上面的代码我们可以看到,其是在程序运行的时候利用runtime的特性动态创建了类以及相关成员变量以及函数方法。
我们利用Lua可以在应用程序中动态地实现一些功能,从而可以绕过app store的审核,但是Lua的开发过程中经常会遇到一些挑战,例如不能打断点调试,贫乏的语法检查。以及之前存在的nil对象crash等等。对于Lua而言我的建议是一些固定或者需要灵活修改的内容可以使用。
Lua还有一个就是可以打patch,修复一些紧急crash.不过还是要慎用。


7、JSPatch

这里有一份关于JSPatch的blog.开源项目的地址:https://github.com/bang590/JSPatch
另外还有一份作者的Object-C转J
总结:JSPatch目前还没有接触过,后续会抽时间好好研究一下。


8、接下来聊一聊百度面试官出的一些面试题目(runtime相关部分)

1、runtime如何通过selector找到对应的IMP地址?
首先,selector是一选择器,准确的来讲是一个objc_selector的结构体指针。
而IMP事实上是一个函数指针,其定义如下id (*IMP)(id, SEL,...)。其中id为实例对象或者类(分别代码实例方法和类方法)。
而在对象中,我们通过isa指针来找个类对象,在内存中类本身也被当作一个对象。而类中有一个方法列表Method.而Method的定义如下

typedef struct objc_method *Method;
struct objc_method {
    SEL method_name                 OBJC2_UNAVAILABLE;  // 方法名
    char *method_types                  OBJC2_UNAVAILABLE;
    IMP method_imp                      OBJC2_UNAVAILABLE;  // 方法实现
}  

从中我们可以看出来,Method成为了SEL与IMP之间的桥梁,通过SEL我们就可以找到IMP。事实上SEL是hash化了的一个唯一key值。就是用来查找IMP的。


2、runtime中的Associate方法关联的对象,需要在主对象dealloc的时候释放吗?
不需要,因为通过Associate关联的对象是存放在另一张hashTable上的,当主对象被释放时,其关联的对象也会被释放。


3、objc中的类方法和实例方法有什么本质区别和联系?
其实在内存中其分布是这样的,所有的类本身是一个对象,实例化的对象通过isa指针来指向这个类对象,这个类对象中包含了实例方法,而类有一个metaClass(元类),这个元类里面存放了类方法。这样类方法和实例方法就这样关联起来了,下面有一张更加详细的图可以说明:
Image


4、objc_msgForward函数是做什么的,直接调用它将会发生什么?
objc中在执行某个函数方法时,会从从类中查找它的IMP,并调用objc_msgSend(),如果没有找到就会去父类中继续查找,直到查找失败时,其就会调用objc_msgForward作为IMP来执行。
直接调用的话会可能导致抛出unrecoginised selector的异常而导致程序crash.


5、runtime如何实现weak变量的自动置nil?
runtime对注册的类,会进行布局,对于weak对象会放入一个hash表中,用weak指向的对象地址作为key,当此对象的引用计数为0的时候会dealloc,进而在这个weak表中找到此对象地址为键的所有weak对象,从而设置为nil.


6、能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime会调用class_setIvarLayout或class_setWeakIvarLayout来处理strong weak引用。所以不能向存在的类中添加实例变量。
运行时的类是可以添加实例变量的,调用class_addIvar函数。但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。


7、如何手动触发一个value的kvo?
我们知道kvo的实现原理,其实是通过runtime的特性,为class动态生成了一个subclass,同时把被观察的对象的isa指向这个子类,而class方法仍然返回原来的类。同时重写被观察的属性的setter方法。其在setter方法里面插入了willChangeValueForKey:didChangeValueForKey:方法.
所以如果想要手动触发一个value的kvo,可以在添加addObserver:forKeyPath:options:context:后,直接调用willChangeValueForKey:didChangeValueForKey:方法。


8、一个objc如何进行内存布局?
实例对象会存在内存中的某处,而它的类也会作为一个对象存放在内存某处,其存放类的实例变量与实例方法。它们两者之前通过isa指针关联起来。而类对象还有一个关联的metaClass,其存放的是类方法。
类对象上面还有superClass,而superClass也有其自己的metaClass.


9、objc中向一个对象发送消息[objc foo]和objc_msgSend()函数之间有什么关系?
objc中在执行某个函数方法时,会从从类中查找它的IMP(函数指针),如果没有找到会在父类中继续查找,找到后,就会调用objc_msgSend()进行消息转发。


10、objc向一个nil对象发送消息会发生什么?
objc向一个nil对象发送消息不会发生crash。似乎看起来什么都不会发生。因为在objc_msgSend消息函数调用时,它会去检查Target,如果Target为nil,则它会cleanup,然后return.


2015-07-26 16:52327