美文网首页
iOS Runtime基础

iOS Runtime基础

作者: 勇往直前888 | 来源:发表于2017-06-13 21:14 被阅读54次

RuntimeObject-C的一种特性,本人并不感冒。不过这块内容却很流行,也是Object-C动态特性的来源,被认为是比Swift好的地方。
平时用不用是一回事,知道这些基础知识还是有好处的,至少跟人聊的时候能打上话。
动态特性带来了方便,但是同时也带来了很大的安全隐患,在使用的时候尽量谨慎一点。
另外,这是底层的函数,像ARC这种偷懒用的好特性就没有了,要注意内存泄漏问题。(基本上无法避免)

Runtime全方位装逼指南
RuntimeLearn
这篇文章写得比较好,基础概念写得比较清晰,值得优先读

iOS动态性(二)可复用而且高度解耦的用户统计埋点实现
统计埋点确实是一个比较典型的应用,这篇文章写得比较清楚

对象、类、元类

  • Object-C是一种面向对象的语言。“一切皆对象”是本质的一点。Object-C就是借助实例,类,元类三级结构来实现这一点的。

  • isa指针是Object-C采用c语言的"结构体"来实现面向对象的方法。

  • 实例的isa指向对应的类,类的isa指向元类。

  • 元类的isa都指向根元类,根元类的isa指向自己。

  • 类和元类除了isa指针,还有一个super_class指针,指向父类(父元类)。根类的父类指向nil

  • 元类保存静态变量和静态类方法

  • 跟元类的父类指向根类,根类的isa指针指向根元类

Object-C类图.jpg

消息发送

  • Objective-C 中的方法调用,不是简单的方法调用,而是发送消息,也就是说,其实[receiver message] 会被编译器转化为: objc_msgSend(receiver, selector)

  • 这个是Objective-C动态特性的本质;在函数调用之前插入一个消息转发,按照对象(id),函数(SEL),参数三级结构实现动态特性。

  • 一些函数定义

void objc_msgSend(void /* id self, SEL op, ... */ );
typedef struct objc_selector *SEL;
typedef struct objc_object *id;

// 下面几个都是将字符串转换为函数指针SEL;根据使用场景选择方便的
// 这个c字符串
SEL 变量名 = sel_registerName(const char *str); // 在c的模块中推荐用
// 下面两个是NSString
SEL 变量名 = NSSelectorFromString(NSString *aSelectorName); // 推荐用这个
SEL 变量名 = @selector(NSString *aSelectorName); // 这个用得比较多,不过难理解,不是很推荐
  • 例子,有个类TestClass,有如下方法和调用
- (void)showSizeWithWidth:(float)aWidth andHeight:(float)aHeight{
    NSLog(@"size is %.2f * %.2f",aWidth, aHeight);
}

TestClass *testObject = [[TestClass alloc] init];
[testObject showSizeWithWidth:110.5f andHeight:200.0f]

也可以用下面的调用方式:

((void (*) (id, SEL, float, float)) objc_msgSend) (testObject, sel_registerName("showSizeWithWidth:andHeight:"), 110.5f, 200.0f);
  • 这个就是Object-C动态特性的来源。id、SEL都是一些指向结构体的指针,objc_msgSend的类型是void,在具体使用的时候需要强制转化为需要的类型。(参数类型,返回值类型都要考虑到)。这里有很大的安全隐患,代码难懂,很容易出错。所以本人一直不建议用。

  • 编译器会根据情况在 objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret或 objc_msgSend_fpret 五个方法中选择一个来调用。如果消息是传递给超类,那么会调用 objc_msgSendSuper方法,如果消息返回值是数据结构,就会调用 objc_msgSendSuper_stret 方法,如果返回值是浮点数,则调用 objc_msgSend_fpret方法。

类的本质

  • Class 也是一个结构体指针类型
typedef struct objc_class *Class;

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
  • ivars:指向该类的成员变量列表。大多数情况可以认为这个也是类的属性列表。不过有说法认为有另外的属性列表,只是这里看不出来。

  • methodLists:指向该类的实例方法列表,它将方法选择器和方法实现地址联系起来。这也是Category实现的原理,同样解释了 Category不能添加属性的原因。

  • protocols:指向该类的协议列表。

这里没有单独的属性列表,给理解带来了困难。如果都是基本类型,可以认为成员变量列表就是属性列表,比如下面的“自动归档”部分处理的那样。
另外一种说法是有单独的属性列表,只是这里没有显示出来。比如“字典转模型”,就使用了属性列表。
class_copyPropertyList和class_copyIvarList的区别

远程调用

  • 有些时候,比如首页,显示的内容由后台决定,实现所谓的“千人千面”

  • 这里的实现基础,就是类Class、方法SEL与字符串NSString的互转

FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);

FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class _Nullable NSClassFromString(NSString *aClassName);
  • 有了Class之后,就可以通过id object = [[Class alloc] init];得到对象

  • 有对象和SEL之后,就可以通过函数执行

- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
  • 客户端先把调用的类和方法在本地实现好,后台发送类名和方法名的字符串,然后根据字符串,实现动态调用。

  • 本人经历过的四五个App中有一个是采用这种方案实现动态首页的。另外,网上也有对这个问题的详细描述。下面这篇链接是比较好的一篇。

iOS应用架构谈 组件化方案
CTMediator

  • 关于组建化,本人更偏向于蘑菇街的方案。原因是这个方案url的编码更自由一点,对应关系可以自定义。另外,有蘑菇街的实践也是一个考虑原因。

蘑菇街 App 的组件化之路
MGJRouter

对象关联

  • Category可以添加方法,但是怎么样添加属性呢?答案是通过对象关联的方法。

  • Category中的属性,只会生成settergetter方法,不会生成成员变量

  • 关联的属性和一个全局变量关联,那个key一般是一个静态全局变量

  • 相关函数:

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object); // 移除所有关联属性,不要轻易使用

// 属性的修饰符,根据情况设置
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

这方面的资料很多,使用也相对简单,比如下面就有一篇:
iOS-OC-Runtime使用小谈(objc_setAssociatedObject)

自动归档

  • 自动归档主要是实现NSCoding协议
@protocol NSCoding

- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER

@end
  • 相关函数
// 把成员变量当做属性,获取列表
Ivar *class_copyIvarList(Class cls, unsigned int *outCount);
// 获得成员变量的名字,带_前缀;这是c字符串
const char *ivar_getName(Ivar v) ;
// 通过KVC获得成员变量的值
- (nullable id)valueForKey:(NSString *)key;
// 通过KVC设置成员变量的值
- (void)setValue:(nullable id)value forKey:(NSString *)key;

字典与模型互转

  • 在类的头文件中定义的属性,不包括额外定义的成员变量

  • 使用属性列表函数,而不是成员列表函数

// 属性列表,不是成员变量列表
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount);
// 获取属性的名字
const char *property_getName(objc_property_t property) ;
  • 可以使用KVC,也可以使用objc_msgSend调用gettersetter函数进行值的读取和设置

字典转模型,自动归档等推荐的第三方库为YYModel,实际用过,确实很方便
YYModel

方法动态解析

给一个对象发消息,就是执行objc_msgSend(id, SEL, ...)函数。使用很小心,id、SEL都正确的情况下,当然没问题。但是,如果出错了呢?
是的,崩溃,崩溃信息一般如下:
unrecognized selector sent to instance ...

  • 不是SEL找不到吗?下面这个函数就是给机会,修改SEL参数
+ (BOOL)resolveInstanceMethod:(SEL)sel;
  • 这个对象没有,其他对象可能有啊。下面这个函数就是给机会,修改id参数
- (id)forwardingTargetForSelector:(SEL)aSelector;
  • 其他对象也没有这个SEL,那么再给机会,id、SEL都改,完全自定义。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
  • 最后,就会抛出异常,就是常说的崩溃
- (void)doesNotRecognizeSelector:(SEL)aSelector;
  • 这块内容具体的应用场景在实际工作中还没有遇到过。上面这些函数都在NSObject.h文件中定义,是基类中的函数。

继承自NSObject的不常用又很有用的函数(2)

方法交换

  • 这个是有使用场景的,最常见的场景是“统计埋点”

iOS动态性(二)可复用而且高度解耦的用户统计埋点实现

  • 这项技术有一个专门的名字叫Method Swizzling,为什么这么叫,原因不清楚。

Objective-C的hook方案(一): Method Swizzling

  • 主要用到的函数:
Method class_getInstanceMethod(Class cls, SEL name);
void method_exchangeImplementations(Method m1, Method m2);

IMP method_getImplementation(Method m);
IMP method_setImplementation(Method m, IMP imp); 
BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                                 const char *types); 
  • 可以考虑用来解决崩溃的问题,比如下面的文章

使用method-swizzling让程序更健壮

  • 方法交换需要放在 +(void)load方法中,并且要用dispatch_once进行保护。道理很简单,交换偶数次不就被还原了吗?

相关文章

网友评论

      本文标题:iOS Runtime基础

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