iOS RunTime之三:使用惯例

作者: SvenLearn | 来源:发表于2016-05-15 15:35 被阅读396次

    Objective-C特性:Runtime
    Method Swizzling 和 AOP 实践


    引子:在OC中如何运用运行时编程

    运行时系统通过三种形式在应用中出现:

    Level-1:OC源码

    OC编译器会捕获class和category定义、protocol声明中的以下信息
     ◈ method selectors
     ◈ instance variable templates
     ◈ ……
    并转化成运行时结构,你需要知道编写的OC代码会变成什么;其中最常见的运行时函数就是发送消息

    Level-2:NSObject Methods - 动态类型

    有一些NSObject方法通过对RTS执行introspection来查询信息,相信大家都用过这些属性/函数:
     ▣ 类型识别:-class-superclass
     ▣ 测试对象的继承、行为和规范
       ◈ - isKindOfClass:
       ◈ - isMemberOfClass:
       ◈ - respondsToSelector:
       ◈ - conformsToProtocol:
     ▣ 获取方法的信息
       ◈ - methodForSelector: // 包含实例方法和类方法
       ◈ + instanceMethodForSelector: // 仅限于实例方法
       ◈ - methodSignatureForSelector: // 包含实例方法和类方法
       ◈ + instanceMethodSignatureForSelector: // 仅限于实例方法

    Level-3:Runtime Functions

    所有的函数可以在Objective-C Runtime Reference找到,这些函数大概可以分为两类:
     ◈ 通过C语言来做一些类似编译器的工作,比如动态生成类、动态为一个已有类添加一个新属性等
     ◈ 另一部分函数构成了NSObject函数的基础
    注意:在这个level使用运行时,需要引入头文件:#import <objc/runtime.h>


    1 动态创建协议、类、对象

    #动态创建类
    // 步骤一:给类分配内存
    Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes );
    
    -----------------------------------------------------------------------------------
    // 步骤二:配置类的成员变量、属性、方法和遵守的协议
    BOOL class_addIvar ( Class cls, const char *name, size_t size, uint8_t alignment, const char *types );
    
    BOOL class_addProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
    void class_replaceProperty ( Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount );
    
    SEL sel_registerName ( const char *str ); // 也可使用更方便的语法糖 @selector
    IMP imp_implementationWithBlock(id block);
    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
    IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
    
    BOOL class_addProtocol(Class cls, Protocol *protocol) ; // 如果实现了协议的required方法,可以此声明class遵守了协议
    -----------------------------------------------------------------------------------
    
    // 步骤三:将类注册到应用中
    void objc_registerClassPair ( Class cls );
    
    // 步骤四:销毁类及其子类 => 确保调用前没有类/子类的实例!!!
    objc_disposeClassPair ( Class cls );
    
    #动态创建对象 - 最佳实例:Json转Model
    // 步骤一:给类(结构体的实例)分配内存 - 类似于+alloc
    id class_createInstance ( Class cls, size_t extraBytes );
    id objc_constructInstance ( Class cls, void *bytes ); // 在指定位置创建类实例
    id object_copy ( id obj, size_t size );
    
    // 步骤二:设置对象的类型
    Class object_setClass ( id obj, Class cls );
    
    // 步骤三:设置对象的成员变量(需根据类型(OC对象/其他)用不同的方法)
    Ivar class_getInstanceVariable ( Class cls, const char *name );   // 实例成员变量
    Ivar class_getClassVariable ( Class cls, const char *name );     // 类变量
    void object_setIvar ( id obj, Ivar ivar, id value );         // 也可使用KVC
    或
    Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
    
    // 步骤四:销毁类实例 - 注意:不会移除相关引用
    void * objc_destructInstance ( id obj );
    id object_dispose ( id obj );
    
    #动态创建协议
    // 步骤一:创建新的协议(如果同名的协议已经存在,则返回nil)
    Protocol * objc_allocateProtocol ( const char *name ); 
    
    // 步骤二:操作协议的属性、方法
    void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
    void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
    void protocol_addProtocol ( Protocol *proto, Protocol *addition );
    
    // 步骤三:注册协议(注意:不同于类,协议注册到运行时系统后不可再修改 and ready to use. )
    void objc_registerProtocol ( Protocol *proto );
    
    // 步骤四:协议关联到类
    BOOL class_addProtocol ( Class cls, Protocol *protocol );
    

    2 遍历属性/成员变量

    #遍历成员变量
    // 步骤一:获取成员变量列表
    Ivar * class_copyIvarList ( Class cls, unsigned int *outCount ); 
    
    // 步骤二:获取成员变量的名字和类型编码
    const char * ivar_getName ( Ivar v );
    const char * ivar_getTypeEncoding ( Ivar v ); // 比如"@\"NSString\""
    
    
    #遍历属性
    // 步骤一:获取属性列表
    objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
    objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
    
    // 步骤二:获取属性的名字和特性信息
    const char * property_getName ( objc_property_t property ); // 返回属性名(区别于成员变量名)
    const char * property_getAttributes ( objc_property_t property ); 
    char * property_copyAttributeValue ( objc_property_t property, const char *attributeName ); 
    objc_property_attribute_t * property_copyAttributeList ( objc_property_t property, unsigned int *outCount );
    

    3 方法变形(Method Swizzling)

    通过这一技术,我们可以在运行时通过修改类的分发表中SEL和IMP的对应关系,来修改方法的实现。
    举例:跟踪在程序中每一个view controller展示给用户的次数,其中的5个关键点有标注

    #import <objc/runtime.h>
    
    @implementation UIViewController (Tracking)
    
    // 1. Swizzling应该总是在+load的dispatch_once中执行
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];  // 类方法:Class class = object_getClass((id)self);
    
            SEL originalSelector = @selector(viewWillAppear:);
            SEL swizzledSelector = @selector(xxx_viewWillAppear:);
    
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
            // 2. 要做什么?- 修改映射关系为:originalSelector->IMP(swizzledMethod), swizzledSelector->IMP(originalMethod)
            BOOL didAddMethod = class_addMethod(class,
                                                originalSelector,
                                                method_getImplementation(swizzledMethod),
                                                method_getTypeEncoding(swizzledMethod));
    
            if (didAddMethod) {
                class_replaceMethod(class,
                                    swizzledSelector,
                                    method_getImplementation(originalMethod),
                                    method_getTypeEncoding(originalMethod));
            } else {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        });
    }
    
    #pragma mark - Method Swizzling
    
    // 3. 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
    - (void)xxx_viewWillAppear:(BOOL)animated {
      // 4. 总是调用方法的原始实现(除非有更好的理由不这么做):这意味着通常我们只是给原来的实现附加一些代码
      [self xxx_viewWillAppear:animated];
      // 5. 输出viewWillAppear:而不是xxx_viewWillAppear:,说明对象是通过viewWillAppear:进入到实现的
      NSLog(@"real SEL name is %@", NSStringFromSelector(_cmd)); 
    }
    
    @end
    

    知识扩展
    Aspect-Oriented Programming(AOP)
    类似记录日志、身份验证、缓存等事务非常琐碎,与业务逻辑无关,很多地方都有,又很难抽象出一个模块,这种程序设计问题,业界给它们起了一个名字叫横向关注点(Cross-cutting concern),AOP的作用就是分离横向关注点来提高模块复用性,它可以在既有的代码添加一些额外的行为(记录日志、身份验证、缓存)而无需修改代码。
    开源工具
    Aspects
    危险性
    Method Swizzling就像一把瑞士小刀,如果使用得当,它会有效地解决问题。但使用不当,将带来很多麻烦。它的危险性主要体现以下几个方面:
     ◈ Method swizzling is not atomic
     ◈ Changes behavior of un-owned code
     ◈ Possible naming conflicts
     ◈ Swizzling changes the method’s arguments
     ◈ The order of swizzles matters
     ◈ Difficult to understand (looks recursive)
     ◈ Difficult to debug
    参考
    从AOP框架学习iOS Runtime



    4 关联对象

    @interface NSObject (CategoryName)
    
    @property (nonatomic,copy) NSString *name;
    
    @end
    ----------------------------------------------------
    @implementation NSObject (CategoryName)
    
    static void *nameKey = "nameKey";  // 设置为静态私有变量,避免外界修改
    - (void)setName:(NSString *)name
    {
        objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (NSString *)name
    {
         return objc_getAssociatedObject(self, &nameKey);
    }
    
    @end
    

    相关文章

      网友评论

        本文标题:iOS RunTime之三:使用惯例

        本文链接:https://www.haomeiwen.com/subject/ephurttx.html