Objective Runtime Reference

本文将列举一些常用的 API

Working with Classes

class_getName

  • 声明:
1
const char * class_getName(Class cls)
  • 作用:获取类名的字符串
  • 返回值:如果类是 Nil 返回空串

class_getSuperclass

  • 声明:
1
Class class_getSuperclass(Class cls)
  • 作用:获取一个类的父类
  • 返回值:如果传入的是根类或者 Nil 返回 Nil
  • Discussion:你可以使用 NSObjectsuperclass 方法代替它

class_isMetaClass

  • 声明:
1
BOOL class_isMetaClass(Class cls)
  • 作用:判断一个类是否是元类

class_getInstanceSize

  • 声明:
1
size_t class_getInstanceSize(Class cls)
  • 作用:返回一个类的大小(单位:字节)
  • 返回值:如果传入 Nil 返回 0

class_addIvar

  • 声明:
1
BOOL class_addIvar(Class cls, const char *name, size_t size, uint8_t alignment, const char *types)
  • 作用:给一个类动态添加一个成员变量
  • 返回值:如果添加成功,返回 YES,否则返回 NO
  • Discussion:这个方法将会在 objc_allocateClassPair 之后 objc_registerClassPair 之前调用,给一个已经存在的类添加成员变量并不支持。而且我们不能给元类添加成员变量。

class_copyIvarList

  • 声明:
1
Ivar * class_copyIvarList(Class cls, unsigned int *outCount)
  • 作用:描述一个类的成员变量
  • 返回值:返回指向第一个成员变量的 Ivar 指针
  • Discussion:传入的 outCount 将告诉我们这个类有多少个成员变量。

### class_getIvarLayout ### class_getWeakIvarLayout ### class_setIvarLayout ### class_setWeakIvarLayout

ivarLayoutweakIvarLayout 分别记录了哪些 ivar 是 strong 或是 weak,都未记录的就是基本类型和 __unsafe_unretained 的对象类型。这两个值可以通过 runtime 提供的几个 API 来访问:

1
2
3
4
const uint8_t *class_getIvarLayout(Class cls) 
const uint8_t *class_getWeakIvarLayout(Class cls)
void class_setIvarLayout(Class cls, const uint8_t *layout)
void class_setWeakIvarLayout(Class cls, const uint8_t *layout)

但我们几乎没可能用到这几个 API,IvarLayout 的值由 runtime 确定,没必要关心它的存在,但为了解决上述问题,我们试着破解了 IvarLayout 的编码方式


class_getProperty

  • 声明:
1
objc_property_t class_getProperty(Class cls, const char *name)
  • 作用:返回一个属性
  • 返回值:如果类没有声明或者属性不存在返回 Nil

class_copyPropertyList

  • 声明:
1
objc_property_t * class_copyPropertyList(Class cls, unsigned int *outCount)
  • 作用:描述一个类的属性列表
  • 返回值:返回一个 objc_property_t 类型的指针数组描述声明的类的属性。传入的 outCount 表示属性个数

class_addMethod

  • 声明:
1
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
  • 作用:给一个类动态的添加方法
  • 返回值:添加成功返回 YES 否则 NO
  • 注意:types 参数是方法的参数类型,详见:Type Encoding
  • Discussion:该方法可以覆盖父类的实现,但是不能添加类中已有的实现如果要改变类中以后的实现请使用 method_setImplementation
  • 示例:
1
2
3
4
5
6
7
void myMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
you can dynamically add it to a class as a method (called resolveThisMethodDynamically) like this:

class_addMethod([self class], @selector(resolveThisMethodDynamically), (IMP) myMethodIMP, "v@:");

class_getInstanceMethod

  • 声明:
1
Method class_getInstanceMethod(Class aClass, SEL aSelector)
  • 作用:从一个类中获取一个实例方法
  • 返回值:当该类中或者父类中都没有该方法 返回 NULL
  • 注意:注意该方法如果在父类中寻找实现,时因为自己调用了 class_copyMethodList 方法而没有寻找到实现。

class_getClassMethod

  • 声明:
1
Method class_getClassMethod(Class aClass, SEL aSelector)
  • 作用:从一个类中获取一个类方法

class_copyMethodList

  • 声明:
1
Method * class_copyMethodList(Class cls, unsigned int *outCount)
  • 作用:描述一个类中的实例方法列表
  • 返回值:返回一个指向 Method 类型的指针数组。
  • 注意:父类的实例方法不会被列入其中
  • 举例:
1
2
3
4
5
6
7
8
9
10
11
12
@interface Person : NSObject

/** 姓名 */
@property (strong, nonatomic) NSString *name;
/** age */
@property (assign, nonatomic) int age;
/** weight */
@property (assign, nonatomic) double weight;
- (void)text;
+ (void)run;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation Person

- (void)text
{
NSLog(@"text ---- %@", self->_name);
}

- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

+ (void)run
{
NSLog(@"run");
}

@end
1
2
3
4
5
6
7
8
unsigned int outCount = 0;

Method *methodList = class_copyMethodList([Person class], &outCount);

for (NSInteger i = 0; i < outCount; i++) {
NSString *name = NSStringFromSelector(method_getName(methodList[i]));
NSLog(@"%@", name);
}

打印结果如下:

1
2
3
4
5
6
7
8
9
2014-11-12 10:57:14.536 test[785:55071] age
2014-11-12 10:57:14.536 test[785:55071] setAge:
2014-11-12 10:57:14.536 test[785:55071] setWeight:
2014-11-12 10:57:14.536 test[785:55071] .cxx_destruct
2014-11-12 10:57:14.536 test[785:55071] dealloc
2014-11-12 10:57:14.536 test[785:55071] name
2014-11-12 10:57:14.537 test[785:55071] weight
2014-11-12 10:57:14.537 test[785:55071] setName:
2014-11-12 10:57:14.537 test[785:55071] text

类方法并没有被打印。


class_replaceMethod

  • 声明:
1
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
  • 作用:替换方法实现

class_getMethodImplementation

  • 声明:
1
IMP class_getMethodImplementation(Class cls, SEL name)
  • 作用:获取方法实现
  • 返回值:返回一个函数指针

class_respondsToSelector

  • 声明:
1
BOOL class_respondsToSelector(Class cls, SEL sel)
  • 作用:判断该类是否有该方法

class_addProtocol

  • 声明:
1
BOOL class_addProtocol(Class cls, Protocol *protocol)
  • 作用:动态添加一个协议

class_addProperty

  • 声明:
1
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
  • 作用:动态添加一个属性

class_replaceProperty

  • 声明:
1
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount)
  • 作用:替换属性

class_conformsToProtocol

  • 声明:
1
BOOL class_conformsToProtocol(Class cls, Protocol *protocol)
  • 作用:判断一个类是否遵守某个协议

class_copyProtocolList

  • 声明:
1
Protocol ** class_copyProtocolList(Class cls, unsigned int *outCount)
  • 作用:获取一个类遵守的协议列表

class_copyProtocolList

  • 声明:
1
Protocol ** class_copyProtocolList(Class cls, unsigned int *outCount)
  • 作用:获取一个类遵守的协议列表


class_copyProtocolList

  • 声明:
1
Protocol ** class_copyProtocolList(Class cls, unsigned int *outCount)
  • 作用:获取一个类遵守的协议列表


Working with Instance Variables

ivar_getName

  • 声明:
1
const char * ivar_getName( Ivar ivar)
  • 作用:获取成员变量的名字

ivar_getTypeEncoding

  • 声明:
1
const char * ivar_getTypeEncoding( Ivar ivar)
  • 作用:获取成员变量的类型


Working with Methods

method_invoke_stret

  • 声明:
1
void method_invoke_stret(id receiver, Method m, ...)
  • 作用:如果返回值时一个结构体,调用这个方法
  • 注意:调用这个方法来实现比 method_getImplementation 要快

method_getName

  • 声明:
1
SEL method_getName( Method method)
  • 作用:获取方法名

method_getImplementation

  • 声明:
1
IMP method_getImplementation( Method method)
  • 作用:获取方法实现
  • 返回值:是一个 IMP 类型的函数指针

method_getNumberOfArguments

  • 声明:
1
unsigned method_getNumberOfArguments( Method method)
  • 作用:获取方法的参数个数

method_exchangeImplementations

  • 声明:
1
void method_exchangeImplementations( Method m1, Method m2)
  • 作用:交换两个方法的方法实现
  • 注意:这时原子形式的,是线程安全的。
1
2
3
4
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);


Working with Selectors

sel_getName

  • 声明:
1
const char* sel_getName(SEL aSelector)
  • 作用:返回方法名

sel_isEqual

  • 声明:
1
BOOL sel_isEqual(SEL lhs, SEL rhs)
  • 作用:判断两个方法选择器是否一样


runtime 实际开发中用法列举

关于 runtime 的知识写了这么多,具体在工作中应该怎样使用呢?

由于苹果是不开源的,因此我们要使用一些系统的框架或者控件时,往往因为其给我们提供的接口太少而不能有效的实现我们的需求。因此,runtime 就派上用场了。

虽然我们无法直接窥探苹果系统内部的结构,但通过 runtime 系统的运行和方法,我们是可以在运行时获取到系统内部类的成员变量、方法、属性以及所遵守的协议的。此时,我们大概就能清楚其内部时如何运作的,这也要归功于 iOS 开发比较讲究的命名规范,我们一看到成员变量的意思,大致就能猜到其作用了。

runtime 基本用法

下面就演示一下如何获取系统内部类的成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 我们便拿 UIButton 开刀好了
UIButton *button = [UIButton buttonWithType:(UIButtonTypeSystem)];

// 下面让我们看看系统的 button 都有些什么东西
unsigned int outCount = 0; // 用来记录成员变量的个数

// 用于获取内部成员变量的列表,返回一个 Ivar 类型的指针数组
Ivar *ivarList = class_copyIvarList([UIButton class], &outCount);

for (NSInteger i = 0; i < outCount; i++) {
NSString *ivarName = @(ivar_getName(ivarList[i]));

NSLog(@"%@", ivarName);
}

打印结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2014-11-12 11:57:36.215 test[912:89888] _externalFlatEdge
2014-11-12 11:57:36.215 test[912:89888] _contentLookup
2014-11-12 11:57:36.215 test[912:89888] _contentEdgeInsets
2014-11-12 11:57:36.216 test[912:89888] _titleEdgeInsets
2014-11-12 11:57:36.216 test[912:89888] _imageEdgeInsets
2014-11-12 11:57:36.216 test[912:89888] _backgroundView
2014-11-12 11:57:36.216 test[912:89888] _floatingContentView
2014-11-12 11:57:36.216 test[912:89888] _contentBackdropView
2014-11-12 11:57:36.216 test[912:89888] _imageView
2014-11-12 11:57:36.216 test[912:89888] _titleView
2014-11-12 11:57:36.217 test[912:89888] _initialized
2014-11-12 11:57:36.217 test[912:89888] _lastDrawingControlState
2014-11-12 11:57:36.217 test[912:89888] _selectGestureRecognizer
2014-11-12 11:57:36.217 test[912:89888] _buttonFlags
2014-11-12 11:57:36.217 test[912:89888] _effectiveContentView
2014-11-12 11:57:36.218 test[912:89888] _maskAnimationView
2014-11-12 11:57:36.218 test[912:89888] _selectionView
2014-11-12 11:57:36.218 test[912:89888] _lazyTitleViewFont
2014-11-12 11:57:36.218 test[912:89888] _contentConstraints
2014-11-12 11:57:36.218 test[912:89888] _internalTitlePaddingInsets

可以看到,其内部是有很多成员变量的,而且通过他们的名字,不难猜到他们的作用。

在工作中我们可以使用 KVC 修改其中某个成员变量的值以达到我们的需求。具体见 KVC

MJExtension 实现字典转模型的原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

// 假设 Person 是模型类
Person *p = [[Person alloc] init];
    
    // 给定一个字典 dict
    NSDictionary *dict;
   
    unsigned int count = 0; // 记录Ivar成员变量的个数
    Ivar *vars = class_copyIvarList([Person class], &count);
   
    // 遍历模型中的属性
    for (int i = 0; i < count; i++) {
        Ivar var = vars[i];
        // 获取属性名
        const char *ivarName = ivar_getName(var); 
        // 获取属性类型
      const char *ivarEncoding = ivar_getTypeEncoding(var); 
     
      // 如果检测到属性类型不为基础值,也就是说模型中还嵌套模型,
      // 则继续遍历模型,通过递归一层一层,找出所有模型的所有属性并赋值。
     
// c-->OC字符串
        NSString *ivarNameOC = [NSString stringWithUTF8String:ivarName];
        // Json解析的字典中的值 与 模型一一对应赋值 运行时遍历Json字典
        [p setValue:dict[ivarNameOC] forKey:ivarNameOC];
    }

runtime 在归档中的应用

在 iOS 开发中,归档是一件比较繁琐的事情,因为有可能你的成员变量有很多几十个甚至上百个。这时我们归档如果再一个一个 encodeObject: 是件很麻烦的事。这时我们通过运行时遍历所有类的成员变量,然后通过 ivar_getName 方法获取变量名,再转成 OC 字符串就可以带入到上述 encodeObject: 方法了。几行代码就可以完成。

黑魔法:Method swizzle

在工作中就遇到过这样的需求。当时苹果刚发布 iOS7,iOS7改动是非常大的,它将立体化改为平面化。促使性能极大提高,但同时,为以前的项目所涉及的图片大多就不适合继续使用了。这时,我们就需要对用户的设备进行检测,如果是 iOS6 则使用旧图片,而 iOS7 则换成新图片。但是项目文件非常多,我们不可能将所有的的系统自带的 -imageNamed: 方法替换成自己的方法。其实我们可以这样去实现:

假设项目中我们的代码时这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import "ViewController.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *faceImageView;
@property (weak, nonatomic) IBOutlet UIImageView *vipImageView;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.faceImageView.image = [UIImage imageNamed:@"face"];
self.vipImageView.image = [UIImage imageNamed:@"vip"];


}

以下是项目正在使用的图片

如果需求要在不改变源代码的情况下,将图片换成:

目前项目运行效果是这样的:

首先,我们需要给 UIImage 写一个分类,添加以下方法,以实现我们的需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@implementation UIImage (AMExtension)

// 写一个 AM_imageNamed:方法
+ (UIImage *)AM_imageNamed:(NSString *)name
{
// 判断设备是否是 iOS7 以上
BOOL isIOS7 = [[UIDevice currentDevice].systemVersion floatValue] >= 7.0;
UIImage *image = nil;

// 如果是,则换成支持 iOS7 的图片
if (isIOS7) {
NSString *newName = [name stringByAppendingString:@"_os7"];
image = [UIImage imageNamed:newName];

} else {
image = [UIImage imageNamed:name];
}

return image;
}

@end

然后,也是关键的一步,就是在分类一开始加载到内存中的时候,我们必须让 runtime 系统了解一件事,就是当程序运行时,系统的 imageNamed: 方法的实现被改成 AM_imageNamed: 方法。这样就大功告成,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@implementation UIImage (AMExtension)

// method swizzle 只需要有一次,在分类载进内存时只需要调用一次
+ (void)load
{
Method originalMethod = class_getClassMethod(self, @selector(imageNamed:));
Method ourMethod = class_getClassMethod(self, @selector(AM_imageNamed:));
method_exchangeImplementations(originalMethod, ourMethod);
}

// 写一个 AM_imageNamed:方法
+ (UIImage *)AM_imageNamed:(NSString *)name
{
// 判断设备是否是 iOS7 以上
BOOL isIOS7 = [[UIDevice currentDevice].systemVersion floatValue] >= 7.0;
UIImage *image = nil;

// 如果是,则换成支持 iOS7 的图片
if (isIOS7) {
NSString *newName = [name stringByAppendingString:@"_os7"];

// 此处需要注意,因为方法实现已经交换所以如果我们想调用系统的方法需要使用我们的方法名
image = [UIImage AM_imageNamed:newName];

} else {
image = [UIImage AM_imageNamed:name];
}

return image;
}

我们并没有改变原来的代码,但是运行效果如下:

同理,你还可以使用 Method Swizzle 去替换系统其他的方法,一些你认为你能写的更好的,在项目中。

Demo 见:Swizzle

文章作者: Ammar
文章链接: http://lizhaoloveit.cn/2014/11/12/RuntimeReference/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ammar's Blog
打赏
  • 微信
  • 支付宝

评论