译文: 低调奢华有内涵的「Runtime」

2019-02-14 17:48:46 浏览数 (1)

这是一篇译文,原文是The down low on Objective-C Runtime ,原文文风俏皮,所以我也没有直译,尽量意译。 当然,我是翻译了这篇文章,但是对Runtime的理解,还是很基础——主要是还没有太多实践,真实开发中几乎也用不到,一如文章所说:「如果可以,避免使用Objective-C的Runtime……」。所以,有问题,我暂时也解答不了。 而至于为什么现在看这个几乎用不着的Runtime?主要是受刺激了。

正文:

你期待看到的是最近更新的Xcode 8 和Swift3.0?你又错了:这次要聊的是我们的老相好——Objective-C!(译者:这篇文章发表于2016年10月4日,那时候刚更新Xcode8)

为什么还要聊OC?Swift3.0不是要干死Objective-C了吗?

此言差矣。Swift虽是天天上头条,但是并不意味着已经完全把曾经和我们朝夕相处的老相好干翻了。为什么非得要互怼,就不能一起愉快滴玩耍吗?一起在「操场」(一语相关)上基情四射。(译者:操场——playground,是Swift的一个工具,用于学习、验证Swift)。

Swift的一个核心功能就是可以和OC进行混编。这两种语言可能根本上不一样,但实际上可以很好地互补。

虽然Swift是用来取代Objective-C的,但苹果依然继续维护Ovjective-C。主要改进了:可以更好地和Swift编译,并添加了很多新特性,如nullability、generics。没有改变OC应用的行为,OC对于开发者来说仍然是一种可读性好的开发语言。

还有,不要忘记,很多激动人心的代码库都是用Objective-C写的,Cocoa本身就是用Objective-C写的,还有很多第三方库,和November Five(译者:一家公司)的内部库。

换言之,虽然Swift很明显会扮演越来越重要的角色,但是Objective-C还是会在未来几年保持影响力。这门古老的语言,还是有一些有用的窍门不为人熟知。而我的最爱,就是下面要讲到的Objective-C Runtime——对于大部分开发者而言,还是有些神秘。

故事要从这里讲起

不久前,当浏览「iOS-developers Slack commnunity」时(译者注:Slack上一个聚集了iOS开发者的地方),我看到有人在Swift频道问一个问题。另外一个人提到了Objective-C的Runtime可能可以解决问题,然后过半的开发者认同答案。

大部分苹果开发者大概听过Runtime——他们知道Runtime的存在——但绝大部分人没有去用过。这个主题不常出现,无论是社区论坛还是苹果自己的文档资料。事实上,苹果还特别声明:

「当你用Objective-C编程的时候,并不需要用到Runtime库」

这就很容易理解为什么有人会这样说:在不了解Objective-C Runtime下去使用它,将会是危险的(会导致程序异常或者崩溃)。因为Runtime允许你访问很多Cocoa或者第三方库的底层特性。

Objective-C的Runtime究竟是什么?

Objective-C的Runtime,是一个用C和「汇编」写的开源库,它为C添加了面向对象的特性,从而创建了Objrctive-C这门语言。

下面引用一些Objective-C Runtime的定义——因为我相信自己是讲不清楚的:

「Objective-C可以从『编译时』、『链接时』再到『运行时』,hold住尽可能多的决策。只要有可能,它都是动态地干活儿的。这就意味着,这门语言不仅需要一个编译器,还需要一个runtime系统,用来执行编译的代码。这个runtime系统就好比如是Objective-C的操作系统,(runtime系统)让这门语言能工作起来。」

上面这个陈述,表明Objective-C是动态干活儿的,也就是说Objective-C是一门动态语言,与之相反,就是Swift、C 、Java等等这类语言。是什么因素决定了一门语言是静态的还是动态?最主要的,就是看方法的调用(什么时候、由谁决定、执行哪段代码,什么时候方法会被执行),还有类型绑定(什么时候决定一个变量会有什么类型)。

静态语言,使用的是静态的方法调度,还有前期类型绑定,意味着编译器在「编译时」就已经定下来了。也就是说,当一个程序正在运行时,你可以100%确保开发者的意图是会被执行的。

而像Objective-C这类动态语言,就有点不一样了。所有的决定都是在Objecitve-C的Runtime库创造的。正因为有了这个库,我们可以自己操纵方法的调度和类型的绑定。

也就是,Objective-C的Runtime,允许大伙儿在runtime(运行时)创建、修改、移除以下内容:

  • 类/Class
  • 方法/Method
  • 实现/Implementation
  • 属性/Properties
  • 实例变量/Instance varivables

另外,你不单可以对自己的代码进行上述修改;Runtime还可以让你操作闭源的代码库,甚至是苹果自己的框架。

杀伤力几何?

现在你可能会想:「wow,听起来好屌」。你是对的,确实屌屌的。但是,如果你曾经是个码农,你更可能想到的是:「wow,听起来好危险」。然后,你又对了。

Objective-C的Runtime就像一把双刃剑,使用它,风险高,回报也高。它赋予你很大的权力,但只要你犯了哪怕一丁点儿错误,都有可能让程序挂掉。Runtime让你有权修改本来不需要修改的代码,还可以访问本来是私有的代码。

听起来很恐怖,不过不是说不要用Runtime了。某位大神曾经讲过:「能力越大,责任越大」。而我们在November Five(译者:一家公司名)也一直尝试使用各种强悍的工具,让事情变得更美好。这里有一些我们过去使用Runtime的真实例子。

用于检视(闭源框架)类的方法、属性;进行学习(Looking under the hood & learning from it)

因为Objective-C的Runtime允许你检视、重写(覆盖)、修改私有或者闭源框架中的方法,这样就可以揭开别人的神秘的面纱,看到某人的代码是如何工作的,所以Runtime是一个很有价值的学习工具。比如,假设你想创建一个类似UITableView,但又有点不一样的组件,这时候你可以用Runtime看一下UITableView是如何构建的。下面就是一个很方便的方法:

代码语言:javascript复制
 (NSArray*)objcruntime_getMethodNames
{
    Class class = [self class];
    NSMutableArray* names = [NSMutableArray new];

    uint count;
    Method* methods = class_copyMethodList(class, &count);

    for (NSUInteger i = 0; i < count;   i)
    {
        Method property = methods[i];

        SEL methodSelector = method_getName(property);
        NSString* methodName = NSStringFromSelector(methodSelector);
        if (methodName)
            [names addObject:methodName];
    }
    free(methods);

    return [names copy];
}

上面的代码,允许你在控制台快速地检索到所有方法名。如果在UITableView中使用,就会看到如下结果:

代码语言:javascript复制
(lldb) po [UITableView objcruntime_getMethodNames]
<__NSArrayI 0x148316000>(
indexPathIsFirstRowInSection:,
indexPathIsLastRowInSection:,
indexPathIsFirstSection:,
indexPathIsLastSection:,
sectionIsLastSection:,
_cnui_applyContactStyle,
_cnui_applyContactStyleStark,
_cnui_adjustContentInset:,
ab_delayedScrollRespectingCaretOfActiveTextViewToCell:atIndexPath:atScrollPosition:animated:,
ab_internalScrollToRowAtIndexPathRespectingCaretOfActiveTextView:atScrollPosition:animated:,
ab_scrollToRowAtIndexPathRespectingCaretOfActiveTextView:atScrollPosition:animated:,
_mapkit_dequeueReusableCellWithIdentifier:,
initWithFrame:,
setFrame:,
layoutSubviews,
.cxx_destruct,
_contentSize,
setBackgroundColor:,
traitCollection,
initWithCoder:,
_populateArchivedSubviews:,

可以看到,这里打印出来的方法,比平常在.h文件看到的多。你可以自己试一下。我们还创建一个NSObject的category,专门用来干这个的,可以在这里找到(译者:链接挂掉了)。

使用关联对象(Working with associated objects)

有时候你会有这样的需求:要在一个类的category添加一个属性,不幸的是,在Objective-C是不能这样干的(译者:通过category可以添加属性,但是不能添加实例变量——因此属性并不能用)。幸运的是,你有associated objects,它允许你在「运行时」将任意值和某个对象关联起来。假设你要创建一个UIImageView的category,用于下载图片。这种情况下,你就可以用associated objects添加一个用于保存图片NSURL的属性,类似如下:

代码语言:javascript复制
NSURL* imageURL = [NSURL URLWithString:@"https://www.trythisforexample.com/images/example_logo.png"];
objc_setAssociatedObject(self.imageView, (__bridge CFStringRef)@"imageURL", imageURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

稍后要拿回URL的话,可以这样做: NSURL* imageURL = objc_getAssociatedObject(self.imageView, (__bridge CFStringRef)@"imageURL");

调试闭源的源代码(Debugging closed source code)

有时候你会遇到程序崩溃,但引起崩溃的代码并不是你写的那部分。如果是开源的代码库,解决办法很简单:你报告这个问题,最好自己解决,然后创建一个pull request(译者:类似在GitHub上贡献开源库的过程)。但如果是闭源的框架,就不好说了。当然你可以报告这个问题,并且保佑很多人也遇到同样的问题,然后祈祷作者可以快速地修复,但确实很难保证问题会得到解决——很可能你也没有时间跟他耗。

之前我们就遇过一遭,我们的应用Appmiral崩溃了,Spotify这个SDK(一个闭源库)导致了这个问题,具体原因是有一个未被识别的selector发送给了一个对象实例,这个对象在这个SDK并没有暴露出来。我们报告了这个错误,并且收到了在下一个版本中会修复这个问题的回复——但悲剧的是,对方没有明确什么时候会发下一版。在节假日期间,我们通常每周会提交多个节日版本,很明显耐心等待人家修复这个问题并不是一种很好的选择。感谢Objective-C的Runtime,我们可以在「运行时」为这个对象添加缺失的方法(方法的实现为空),这样就可以防止这个崩溃了。虽然不是一种理想的解决方案,但在等真正导致问题的修复发布前,Runtime确实帮忙防止了成千上万这种崩溃(译者:通过class_addMethod()函数,可以在 (BOOL)resolveInstanceMethod:(SEL)sel方法中动态地添加方法实现)。

JSONModel

很多流行的第三方库都是利用Objective-C的Runtime实现的,JSONModel就是我们常用到的一个。有人可能不知道,JSONModel允许你轻松地从JSON创建数据模型。实现原理是:Objective-C的Runtime,会在「运行时」读取对象的属性,并填充从JSON获取的值。

要知道它具体是怎么实现的,只需要看一下JSONModel.m文件的__inspectProperties方法就可以了。下面是一个简单的截取:

代码语言:javascript复制
// inspect inherited properties up to the JSONModel class
while (class != [JSONModel class]) {
    //JMLog(@"inspecting: %@", NSStringFromClass(class));

    unsigned int propertyCount;
    objc_property_t *properties = class_copyPropertyList(class, &propertyCount);

    //loop over the class properties
    for (unsigned int i = 0; i < propertyCount; i  ) {

        JSONModelClassProperty* p = [[JSONModelClassProperty alloc] init];

        //get property name
        objc_property_t property = properties[i];
        const char *propertyName = property_getName(property);
        p.name = @(propertyName);

        //JMLog(@"property: %@", p.name);

        //get property attributes
        const char *attrs = property_getAttributes(property);
        NSString* propertyAttributes = @(attrs);
        NSArray* attributeItems = [propertyAttributes componentsSeparatedByString:@","];

总结(Wrapping up)

通过上面这些例子,可以总结一下Objective-C的Runtime了!你记住了什么了吗?

  • 如果可以,避免使用Objective-C的Runtime,只有在手头上的问题不能用其他方法解决时,才使用它(小心使用)。
  • 当你使用Runtime时,要清醒知道自己在做什么。
  • 说真的,使用Runtime的时候,确保知道自己在做什么!风险很高的,而且很多事情可能会出错。
  • 不要用来修改苹果框架的私有方法,你的App上架时会被拒的。
  • 如果你交换(swizzle)了苹果框架的方法,始终要调用原来的方法实现。要知道系统更新会对你的应用产生严重影响。

你还想研究更多关于Objective-C Runtime的内容吗?如果你是一个Cocoa开发者,最好的学习资源当然是苹果自己的API文档。或者点击这些优秀的博文,关于associated objects和method swzzling,还有这个Objective-C Runtime的基本应用(译者:最后这个链接挂掉了,不用点了)。

0 人点赞