深入理解Objective-C:优化你的代码
开篇
只要用到Objective-C,我们每天都会跟方法调用打交道。我们都知道Objective-C的方法决议是动态的,但是在底层一个方法究竟是怎么找到的,方法缓存又是怎么运作的却鲜为人知。
本文主要从源码角度探究了Objective-C在runtime层的方法决议(Method resolving)过程和方法缓存(Method cache)的实现。
介绍
本文系学习Objective-C的runtime源码时整理所成,主要剖析了Objective-C在runtime层的方法决议过程和方法缓存。
我们都知道,在Objective-C里调用一个方法是这样的:
[object methodA];
这表示我们想去调用object的methodA。
但是在Objective-C里面调用一个方法到底意味着什么呢,是否和C++一样,任何一个非虚方法都会被编译成一个唯一的符号,在调用的时候去查找符号表,找到这个方法然后调用呢?
答案是否定的。在Objective-C里面调用一个方法的时候,runtime层会将这个调用翻译成
objc_msgSend(id self, SEL op, ...)
而objc_msgSend具体又是如何分发的呢? 我们来看下runtime层objc_msgSend的源码。
在objc-msg-arm.s中,objc_msgSend的代码如下:
ENTRY objc_msgSend
# check whether receiver is nil
teq a1, #
beq LMsgSendNilReceiver # save registers and load receiver's class for CacheLookup
stmfd sp!, {a4,v1}
ldr v1, [a1, #ISA] # receiver is non-nil: search the cache
CacheLookup a2, v1, LMsgSendCacheMiss # cache hit (imp in ip) and CacheLookup returns with nonstret (eq) set, restore registers and call
ldmfd sp!, {a4,v1}
bx ip # cache miss: go search the method lists
LMsgSendCacheMiss:
ldmfd sp!, {a4,v1}
b _objc_msgSend_uncached LMsgSendNilReceiver:
mov a2, #
bx lr LMsgSendExit:
END_ENTRY objc_msgSend STATIC_ENTRY objc_msgSend_uncached # Push stack frame
stmfd sp!, {a1-a4,r7,lr}
add r7, sp, # # Load class and selector
ldr a3, [a1, #ISA] /* class = receiver->isa */
/* selector already in a2 */
/* receiver already in a1 */ # Do the lookup
MI_CALL_EXTERNAL(__class_lookupMethodAndLoadCache3)
MOVE ip, a1 # Prep for forwarding, Pop stack frame and call imp
teq v1, v1 /* set nonstret (eq) */
ldmfd sp!, {a1-a4,r7,lr}
bx ip
从上述代码中可以看到,objc_msgSend(就arm平台而言)的消息分发分为以下几个步骤:
判断receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象
从缓存里寻找,找到了则分发,否则
利用objc-class.mm中_class_lookupMethodAndLoadCache3方法去寻找selector
- 如果支持GC,忽略掉非GC环境的方法(retain等)
- 从本class的method list寻找selector,如果找到,填充到缓存中,并返回selector,否则
- 寻找父类的method list,并依次往上寻找,直到找到selector,填充到缓存中,并返回selector,否则
- 调用_class_resolveMethod,如果可以动态resolve为一个selector,不缓存,方法返回,否则
- 转发这个selector,否则
报错,抛出异常
缓存
从上面的分析中我们可以看到,当一个方法在比较“上层”的类中,用比较“下层”(继承关系上的上下层)对象去调用的时候,如果没有缓存,那么整个查找链是相当长的。就算方法是在这个类里面,当方法比较多的时候,每次都查找也是费事费力的一件事情。
考虑下面的一个调用过程:
for ( int i = ; i < ; ++i) {
MyClass *myObject = myObjects[i];
[myObject methodA];
}
当我们需要去调用一个方法数十万次甚至更多地时候,查找方法的消耗会变的非常显著。
就算我们平常的非大规模调用,除非一个方法只会调用一次,否则缓存都是有用的。在运行时,那么多对象,那么多方法调用,节省下来的时间也是非常可观的。
追本溯源,何为方法缓存
本着源码面前,了无秘密的原则,我们看下源码中的方法缓存到底是什么,在objc-cache.mm中,objc_cache的定义如下:
struct objc_cache {
uintptr_t mask; /* total = mask + 1 */
uintptr_t occupied;
cache_entry *buckets[];
};
嗯,objc_cache的定义看起来很简单,它包含了下面三个变量:
1)、mask:可以认为是当前能达到的最大index(从0开始的),所以缓存的size(total)是mask+1
2)、occupied:被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而occupied表示当前被占用的数目
3)、buckets:用数组表示的hash表,cache_entry类型,每一个cache_entry代表一个方法缓存
(buckets定义在objc_cache的最后,说明这是一个可变长度的数组)
而cache_entry的定义如下:
typedef struct {
SEL name; // same layout as struct old_method
void *unused;
IMP imp; // same layout as struct old_method
} cache_entry;
cache_entry定义也包含了三个字段,分别是:
1)、name,被缓存的方法名字
2)、unused,保留字段,还没被使用。
3)、imp,方法实现
缓存和散列
缓存的存储使用了散列表。
为什么要用散列表呢?因为散列表检索起来更快,我们来看下是方法缓存如何散列和检索的:
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot because the
// minimum size is 4 and we resized at 3/4 full.
buckets = (cache_entry **)cache->buckets;
for (index = CACHE_HASH(sel, cache->mask);
buckets[index] != NULL;
index = (index+) & cache->mask)
{
// empty
}
buckets[index] = entry;
这是往方法缓存里存放一个方法的代码片段,我们可以看到sel被散列后找到一个空槽放在buckets中,而CACHE_HASH的定义如下:
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
这段代码就是利用了sel的指针地址和mask做了一下简单计算得出的。
而从散列表取缓存则是利用汇编语言写成的(是为了高度优化objc_msgSend而使用汇编的)。我们看objc-msg-arm.mm 里面的CacheLookup方法:
.macro CacheLookup /* selReg, classReg, missLabel */ MOVE r9, $, LSR # /* index = (sel >> 2) */
ldr a4, [$, #CACHE] /* cache = class->cache */
add a4, a4, #BUCKETS /* buckets = &cache->buckets */ /* search the cache */
/* a1=receiver, a2 or a3=sel, r9=index, a4=buckets, $1=method */
:
ldr ip, [a4, #NEGMASK] /* mask = cache->mask */
and r9, r9, ip /* index &= mask */
ldr $, [a4, r9, LSL #] /* method = buckets[index] */
teq $, # /* if (method == NULL) */
add r9, r9, # /* index++ */
beq $ /* goto cacheMissLabel */ ldr ip, [$, #METHOD_NAME] /* load method->method_name */
teq $, ip /* if (method->method_name != sel) */
bne 1b /* retry */ /* cache hit, $1 == method triplet address */
/* Return triplet in $1 and imp in ip */
ldr ip, [$, #METHOD_IMP] /* imp = method->method_imp */ .endmacro
虽然是汇编,但是注释太详尽了,理解起来并不难,还是求hash,去buckets里找,找不到按照hash冲突的规则继续向下,直到最后。
为什么
了解了方法缓存的定义之后,我们提出几个问题并一一解答
方法缓存存在什么地方?
让我们去翻看类的定义,在Objective-C 2.0中,Class的定义大致是这样的(见objc-runtime.mm)struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};我们看到在类的定义里就有cache字段,没错,类的所有缓存都存在metaclass上,所以每个类都只有一份方法缓存,而不是每一个类的object都保存一份。
父类方法的缓存只存在父类么,还是子类也会缓存父类的方法?
在第一节对objc_msgSend的追溯中我们可以看到,即便是从父类取到的方法,也会存在类本身的方法缓存里。而当用一个父类对象去调用那个方法的时候,也会在父类的metaclass里缓存一份。类的方法缓存大小有没有限制?
要回答这个问题,我们需要再看一下源码,在objc-cache.mm有一个变量定义如下:/* When _class_slow_grow is non-zero, any given cache is actually grown
* only on the odd-numbered times it becomes full; on the even-numbered
* times, it is simply emptied and re-used. When this flag is zero,
* caches are grown every time. */
static const int _class_slow_grow = ;其实不用再看进一步的代码片段,仅从注释我们就可以看到问题的答案。注释中说明,当_class_slow_grow是非0值的时候,只有当方法缓存第奇数次满(使用的槽位超过3/4)的时候,方法缓存的大小才会增长(会清空缓存,否则hash值就不对了);当第偶数次满的时候,方法缓存会被清空并重新利用。 如果_class_slow_grow值为0,那么每一次方法缓存满的时候,其大小都会增长。
所以单就问题而言,答案是没有限制,虽然这个值被设置为1,方法缓存的大小增速会慢一点,但是确实是没有上限的。为什么类的方法列表不直接做成散列表呢,做成list,还要单独缓存,多费事?
这个问题么,我觉得有以下三个原因:- 散列表是没有顺序的,Objective-C的方法列表是一个list,是有顺序的;Objective-C在查找方法的时候会顺着list依次寻找,并且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的顺序就没法保证。
- list的方法还保存了除了selector和imp之外其他很多属性
- 散列表是有空槽的,会浪费空间
缓存 - 性能优化的万金油?
非也,就算有了有了Objective-C本身的方法缓存,我们还是有很多调用方法的优化空间,对于这件事情,这篇文章讲的非常详细,大家可以自行移步观摩http://www.mulle-kybernetik.com/artikel/Optimization/opti-3-imp-deluxe.html (强烈推荐,虽然我们一般不会遇到需要这么强度优化的地方,但是这种精神和思想是值得我们学习的)
深入理解Objective-C:优化你的代码的更多相关文章
- 优化C/C++代码的小技巧
说明: 无意看到一篇小短文,猜测作者应该是一个图形学领域的程序员或专家,介绍了在光线(射线)追踪程序中是如何优化C/C++代码的.倒也有一些参考意义,当然有的地方我并不赞同或者说我也不完全理解,原文在 ...
- 优化C/C++代码的小技巧(转)
源:http://www.cnblogs.com/lizhenghn/p/3969531.html 说明: 无意看到一篇小短文,猜测作者应该是一个图形学领域的程序员或专家,介绍了在光线(射线)追踪程序 ...
- [Android] Android开发优化之——从代码角度进行优化
通常我们写程序,都是在项目计划的压力下完成的,此时完成的代码可以完成具体业务逻辑,但是性能不一定是最优化的.一般来说,优秀的程序员在写完代码之后都会不断的对代码进行重构.重构的好处有很多,其中一点,就 ...
- 深入理解dijkstra+堆优化
深入理解dijkstra+堆优化 其实就这几种代码几种结构,记住了完全就可以举一反三,所以多记多练多优化多思考. Dijkstra 对于一个有向图或无向图,所有边权为正(边用邻接矩阵的形式给出), ...
- CssStats – 分析和优化网站 CSS 代码的利器
CssStats 是一个在线的 CSS 代码分析工具,你只需要输入网址或者直接 CSS 地址即可进行 CSS 代码的全方位分析,是前端开发人员和网页设计师分析网站 CSS 代码的利器,可以统计出 CS ...
- 仅个人兴趣,自己通过搜索他人的成果,结合自己的理解,来分析discuz的代码。
仅个人兴趣,自己通过搜索他人的成果,结合自己的理解,来分析discuz的代码. discuz 版本: 3.2
- 从原理上理解MySQL的优化建议
从原理上理解MySQL的优化建议 预备知识 B+树索引 mysql的默认存储引擎InnoDB使用B+树来存储数据的,所以在分析优化建议之前,了解一下B+树索引的基本原理. 上图是一个B+树索引示意图, ...
- 如何优化C语言代码(程序员必读)
1.选择合适的算法和数据结构 应该熟悉算法语言,知道各种算法的优缺点,具体资料请参见相应的参考资料,有很多计算机书籍上都有介绍.将比较慢的顺序查找法用较快的二分查找或乱序查找法代替,插入排序或冒泡排序 ...
- 解析Android开发优化之:从代码角度进行优化的技巧
下面我们就从几个方面来了解Android开发过程中的代码优化,需要的朋友参考下 通常我们写程序,都是在项目计划的压力下完成的,此时完成的代码可以完成具体业务逻辑,但是性能不一定是最优化的.一般来说 ...
随机推荐
- 【javascript 函数基础知识】
函数实际上是对象,每个函数都是 Function 类型的实例,而且都会与其他引用类型一样具有属性和方法.由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定. [概念标签] ...
- WARNING L15: MULTIPLE CALL TO SEGMENT
原网页:http://www.cnblogs.com/CuriosityWzk/archive/2011/12/25/2301090.html WARNING L15: MULTIPLE CALL T ...
- [原博客] POJ 2484 A Funny Game
题目链接题意:有n个硬币排成一圈,两个人轮流操作,每次可以取走一个或者相邻的连个硬币(只算最开始相邻的,取之后才相邻的不算),问先手必胜还是必败. 这个题可以证明若n>=3,则先手必败.对称博弈 ...
- SQL Server GOTO使用实例详解
GOTO命令用来改变程序执行的流程,使程序跳到标识符指定的程序行再继续往下执行.语法:GOTO 标识符标识符需要在其名称后加上一个冒号“:”.例如:“33:”,“loving:.SQL语句如下:DE ...
- Linux中的随机数文件 /dev/random /dev/urandom
Linux中的随机数可以从两个特殊的文件中产生,一个是/dev/urandom.另外一个是/dev/random.他们产生随机数的原理是利用当前系统的熵池来计算出固定一定数量的随机比特,然后将这些比特 ...
- logstash 贪婪匹配
05:50:47 192.168.5.116 GET /Hotel/HotelDisplay/cncqcqb230 - 80 - 192.168.9.2 Mozilla/5.0+(Macintosh; ...
- 如何实现Android重启应用程序代码 ?
Intent i = getBaseContext().getPackageManager() .getLaunchIntentForPackage(getBaseContext().getPack ...
- Android Intent.FLAG_NEW_TASK详解,包括其他的标记的一些解释
本文大部分参考自 http://blog.csdn.net/mayingcai1987/article/details/6200909 ,对原文中的讲解FLAG_NEW_TASK地方加了一些自己的观点 ...
- 【CF】121 Div.1 C. Fools and Roads
题意是给定一棵树.同时,给定如下k个查询: 给出任意两点u,v,对u到v的路径所经过的边进行加计数. k个查询后,分别输出各边的计数之和. 思路利用LCA,对cnt[u]++, cnt[v]++,并对 ...
- Python urllib2写爬虫时候每次request open以后一定要关闭
最近用python urllib2写一个爬虫工具,碰到运行一会程序后就会出现scoket connection peer reset错误.经过多次试验发现原来是在每次request open以后没有及 ...