Method Swizzling 剖析
一、背景介绍
关于Method Swizzling的文章一大堆,讲的非常好的也数不胜数。不过,很多人只是会用,知道一些注意点。深入一点问的话,估计就答得不好。归其原因就是对Method Swizzling 理解的不够透彻。本文些的初衷就是为了让大家更容易理解,仅此而已。如若有错之处,还望指正。
二、经典代码
SEL originalSelector = @selector(applicationDidBecomeActive:);
SEL swizzledSelector = @selector(my_applicationDidBecomeActive:); Method originalMethod = class_getInstanceMethod([self class], originalSelector);
Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector); BOOL didAddMethod =
class_addMethod([self class],
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod)); if (didAddMethod) {
class_replaceMethod([self class],
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
上面这段代码是相对较完善的一段,也有同学会写成下面这种:
SEL originalSelector = @selector(applicationDidBecomeActive:);
SEL swizzledSelector = @selector(my_applicationDidBecomeActive:); Method originalMethod = class_getInstanceMethod([self class], originalSelector);
Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector); method_exchangeImplementations(originalMethod, swizzledMethod);
大部分人也是懂原因的:如果Swizzling的 方法是父类里的,而子类里面没有重写,第二种写法就有可能不妥。因为按照第二种写法,会把父类的方法Swizzling,这样会导致所有的子类都会受影响,这个可能不是你想要的。
三、解释一下细节
大部分人只是知道怎么用,用的时候copy一下,但是要有刨根问底的精神,知其然,知其所以然。
SEL :类成员方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号。在内存中每个类的方法都存储在类对象中,每个方法都有一个与之对应的SEL类型的数据,根据一个SEL数据就可以找到对应的方法地址,进而调用方法。
SEL类型的定义: typedef struct objc_selector *SEL
IMP:Implement缩写,表示指向方法的实现地址,可通过IMP来调用方法。
Method:An opaque type that represents a method in a class definition。
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
我们看一下类里面是怎样存放Method的:
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;
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists 这个就是类存放Method的数组。
可以看到 Class 、SEL、Method等 都是结构体,IMP是函数 的指针地址。
@selector(applicationDidBecomeActive:) 获取applicationDidBecomeActive:方法的 SEL。
class_getInstanceMethod([self class], originalSelector); 获取applicationDidBecomeActive:方法的 Method。
以上都是一些简单概念,接下来我们讲下重点,也就是几个系统API。
class_getInstanceMethod源码:
/***********************************************************************
* class_getInstanceMethod. Return the instance method for the
* specified class and selector.
**********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel)
{
//先判读 传入的cls 和 sel是否为nil,如果nil 直接return nil了
if (!cls || !sel) return nil; // This deliberately avoids +initialize because it historically did so. // This implementation is a bit weird because it's the only place that
// wants a Method instead of an IMP.
// 先从方法缓存列表里面寻找 ,找到return ,找不到继续
Method meth;
meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache);
if (meth == (Method)1) {
// Cache contains forward:: . Stop searching.
return nil;
} else if (meth) {
return meth;
}
//根据下面再次从缓存寻找,也知道lookUpImpOrNil的作用就是从 objc_method_list寻找,但并没有直接return,而是找完丢入了方法缓存列表里了
// Search method lists, try method resolver, etc.
lookUpImpOrNil(cls, sel, nil,
NO/*initialize*/, NO/*cache*/, YES/*resolver*/); //再次从方法缓存列表里面寻找
meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache);
if (meth == (Method)1) {
// Cache contains forward:: . Stop searching.
return nil;
} else if (meth) {
return meth;
} return _class_getMethod(cls, sel);
}
我们来看下lookUpImpOrNil 具体干了什么:
/***********************************************************************
* lookUpImpOrNil.
* Like lookUpImpOrForward, but returns nil instead of _objc_msgForward_impcache
**********************************************************************/
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
里面搞了个传送门lookUpImpOrForward,我们看看lookUpImpOrForward做了什么:
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
Class curClass;
IMP methodPC = nil;
Method meth;
bool triedResolver = NO; methodListLock.assertUnlocked(); // Optimistic cache lookup
if (cache) {
methodPC = _cache_getImp(cls, sel);
if (methodPC) return methodPC;
} // Check for freed class
if (cls == _class_getFreedObjectClass())
return (IMP) _freedHandler; // Check for +initialize
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
} // The lock is held to make method-lookup + cache-fill atomic
// with respect to method addition. Otherwise, a category could
// be added but ignored indefinitely because the cache was re-filled
// with the old value after the cache flush on behalf of the category.
retry:
methodListLock.lock(); // Try this class's cache. methodPC = _cache_getImp(cls, sel);
if (methodPC) goto done; // Try this class's method lists. meth = _class_getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, cls, meth, sel);
methodPC = method_getImplementation(meth);
goto done;
} // Try superclass caches and method lists. curClass = cls;
while ((curClass = curClass->superclass)) {
// Superclass cache.
meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
if (meth) {
if (meth != (Method)1) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, curClass, meth, sel);
methodPC = method_getImplementation(meth);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
} // Superclass method list.
meth = _class_getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, curClass, meth, sel);
methodPC = method_getImplementation(meth);
goto done;
}
} // No implementation found. Try method resolver once. if (resolver && !triedResolver) {
methodListLock.unlock();
_class_resolveMethod(cls, sel, inst);
triedResolver = YES;
goto retry;
} // No implementation found, and method resolver didn't help.
// Use forwarding. _cache_addForwardEntry(cls, sel);
methodPC = _objc_msgForward_impcache; done:
methodListLock.unlock(); return methodPC;
}
代码有点长,我不想一一解释了,简单描述一下:
1.就是先从缓存列表里面寻找Method,找到就return;
2.找不到就从类的struct objc_method_list **methodLists里面寻找,找到return;
3.如果还是找不到,就去父类 缓存找,找不到就去父类 方法列表 找;
4.如果还是找不到,再去父类的父类的 缓存找。。。 while 这一块相当于一个递归吧,一层一层往上撸。
5.代码里面有log_and_fill_cache 或是_cache_addForwardEntry ,他们作用就是将 找到的Method 加到缓存里。
6.另外我们看了锁 和 强大的 goto,goto是 C 里面强大的 “时光传送门”,菜鸟慎用 :) 大神也有坠机时候。
感觉有点写不下去了,捂着眼睛继续吧,来~,我们看一下log_and_fill_cache的实现:
/***********************************************************************
* log_and_fill_cache
* Log this method call. If the logger permits it, fill the method cache.
* cls is the method whose cache should be filled.
* implementer is the class that owns the implementation in question.
**********************************************************************/
static void
log_and_fill_cache(Class cls, Class implementer, Method meth, SEL sel)
{
#if SUPPORT_MESSAGE_LOGGING
if (objcMsgLogEnabled) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
_cache_fill (cls, meth, sel);
}
我不管了,继续_cache_fill:
/***********************************************************************
* _cache_fill. Add the specified method to the specified class' cache.
* Returns NO if the cache entry wasn't added: cache was busy,
* class is still being initialized, new entry is a duplicate.
*
* Called only from _class_lookupMethodAndLoadCache and
* class_respondsToMethod and _cache_addForwardEntry.
*
* Cache locks: cacheUpdateLock must not be held.
**********************************************************************/
bool _cache_fill(Class cls, Method smt, SEL sel)
{
uintptr_t newOccupied;
uintptr_t index;
cache_entry **buckets;
cache_entry *entry;
Cache cache; cacheUpdateLock.assertUnlocked(); // Never cache before +initialize is done
if (!cls->isInitialized()) {
return NO;
} // Keep tally of cache additions
totalCacheFills += 1; mutex_locker_t lock(cacheUpdateLock); entry = (cache_entry *)smt; cache = cls->cache; // Make sure the entry wasn't added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
// Don't use _cache_getMethod() because _cache_getMethod() doesn't
// return forward:: entries.
if (_cache_getImp(cls, sel)) {
return NO; // entry is already cached, didn't add new one
} // Use the cache as-is if it is less than 3/4 full
newOccupied = cache->occupied + 1;
if ((newOccupied * 4) <= (cache->mask + 1) * 3) {
// Cache is less than 3/4 full.
cache->occupied = (unsigned int)newOccupied;
} else {
// Cache is too full. Expand it.
cache = _cache_expand (cls); // Account for the addition
cache->occupied += 1;
} // 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+1) & cache->mask)
{
// empty
}
buckets[index] = entry; return YES; // successfully added new cache entry
}
上面就是将Method加到cache里的,细节我就不说了,不过有一点我们还是要说下: Use the cache as-is if it is less than 3/4 full,是的,针对缓存,苹果是有个机制的,当缓存达到3/4时候会释放的,重新来,是不是想起了NSCache?
_cache_addForwardEntry这个方法:
/***********************************************************************
* _cache_addForwardEntry
* Add a forward:: entry for the given selector to cls's method cache.
* Does nothing if the cache addition fails for any reason.
* Called from class_respondsToMethod and _class_lookupMethodAndLoadCache.
* Cache locks: cacheUpdateLock must not be held.
**********************************************************************/
void _cache_addForwardEntry(Class cls, SEL sel)
{
cache_entry *smt; smt = (cache_entry *)malloc(sizeof(cache_entry));
smt->name = sel;
smt->imp = _objc_msgForward_impcache;
if (! _cache_fill(cls, (Method)smt, sel)) { // fixme hack
// Entry not added to cache. Don't leak the method struct.
free(smt);
}
}
一样的,也是调用了_cache_fill,嗯!关于class_getInstanceMethod就聊到这里,打住!
OBJC_EXPORT IMP method_getImplementation(Method m)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
知道Method是个结构体以及其结构,就能知道这个方法实现其实很简单:
IMP method_getImplementation(Method m)
{
if (!m) return nil;
return oldmethod(m)->method_imp;
}
//就这么简单
我们看看method_exchangeImplementations的实现:
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return; rwlock_writer_t lock(runtimeLock);
// 这不是 a b容器里的内容交换么,找到了第三个容器c来帮忙,是不是源码也是很简单的,瞄一眼就知道了。
IMP m1_imp = m1->imp;
m1->imp = m2->imp;
m2->imp = m1_imp; // RR/AWZ updates are slow because class is unknown
// Cache updates are slow because class is unknown
// fixme build list of classes whose Methods are known externally? flushCaches(nil); updateCustomRR_AWZ(nil, m1);
updateCustomRR_AWZ(nil, m2);
}
交换两个Method 结构体里面的IMP而已。
class_addMethod 我们再来看一下这个函数:
/***********************************************************************
* class_addMethod
**********************************************************************/
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
IMP old;
if (!cls) return NO; old = _class_addMethod(cls, name, imp, types, NO);
return !old;
}
/***********************************************************************
* class_addMethod
**********************************************************************/
static IMP _class_addMethod(Class cls, SEL name, IMP imp,
const char *types, bool replace)
{
old_method *m;
IMP result = nil; if (!types) types = ""; mutex_locker_t lock(methodListLock);//加锁
// 先判断该方法是否存在这个类里,如果存在,只改下Method 的IMP就可以
if ((m = _findMethodInClass(cls, name))) {
// already exists
// fixme atomic
result = method_getImplementation((Method)m);
if (replace) {
method_setImplementation((Method)m, imp);
}
} else {
// 如果不存在这个类里,则要动态为这个class 插入一个方法
// fixme could be faster
old_method_list *mlist =
(old_method_list *)calloc(sizeof(old_method_list), 1);
mlist->obsolete = fixed_up_method_list;
mlist->method_count = 1;
mlist->method_list[0].method_name = name;
mlist->method_list[0].method_types = strdup(types);
mlist->method_list[0].method_imp = imp;
// 向类里插入 方法
_objc_insertMethods(cls, mlist, nil);
// 清空 类的缓存
if (!(cls->info & CLS_CONSTRUCTING)) {
flush_caches(cls, NO);
} else {
// in-construction class has no subclasses
flush_cache(cls);
}
result = nil;
} return result;
}
待续。。。。
Method Swizzling 剖析的更多相关文章
- runtime 第四部分method swizzling
接上一篇 http://www.cnblogs.com/ddavidXu/p/5924597.html 转载来源http://www.jianshu.com/p/6b905584f536 http:/ ...
- Objective-C Runtime 运行时之四:Method Swizzling
理解Method Swizzling是学习runtime机制的一个很好的机会.在此不多做整理,仅翻译由Mattt Thompson发表于nshipster的Method Swizzling一文. Me ...
- 【原】iOS动态性(三) Method Swizzling以及AOP编程:在运行时进行代码注入
概述 今天我们主要讨论iOS runtime中的一种黑色技术,称为Method Swizzling.字面上理解Method Swizzling可能比较晦涩难懂,毕竟不是中文,不过你可以理解为“移花接木 ...
- Method Swizzling
学习博客: http://www.cocoachina.com/ios/20160121/15076.html (这个作者太牛了,写了我一直想知道的类簇的swizz方法) 一. 一般的swizz 先给 ...
- Objective-C 利用OC的消息机制,使用Method Swizzling进行方法修改
功能:修改父类不可修改函数方法,函数方法交换 应用场景:假如我们使用的他人提供一个的framework,.m已被打包成二进制.a无法修改源码,只留下.h头文件,那假如代码中某个函数出现了问题可以通过这 ...
- Method Swizzling (方法调配)
Method Swizzling是改变一个selector的实际实现的技术.通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现. 例如,我们想跟踪在程序中每 ...
- iOS中AOP与Method Swizzling 项目中的应用
引子:项目中需要对按钮点击事件进行统计分析,现在项目中就是在按钮的响应代码中添加点击事件,非常繁琐.所以使用了AOP(面向切面编程),将统计的业务逻辑统一抽离出来. 项目中添加的开源库:https:/ ...
- Method Swizzling和AOP(面向切面编程)实践
Method Swizzling和AOP(面向切面编程)实践 参考: http://www.cocoachina.com/ios/20150120/10959.html 上一篇介绍了 Objectiv ...
- iOS运行时与method swizzling
C语言是静态语言,它的工作方式是通过函数调用,这样在编译时我们就已经确定程序如何运行的.而Objective-C是动态语言,它并非通过调用类的方 法来执行功能,而是给对象发送消息,对象在接收到消息之后 ...
随机推荐
- 【BZOJ1217】[HNOI2003]消防局的设立 树形DP
[BZOJ1217][HNOI2003]消防局的设立 Description 2020年,人类在火星上建立了一个庞大的基地群,总共有n个基地.起初为了节约材料,人类只修建了n-1条道路来连接这些基地, ...
- div img span 垂直居中问题
div设置height,line-height影响块元素内的文本垂直居中,块下的a,img,span等行内元素是没能垂直居中,用了vertical-align:middle;可以处理div等块级元素下 ...
- disable的错误使用
表单中的input设为disable后数据无法提交. 如果需要设置无法修改效果,但又想表单提交数据,可以设置readonly.
- Python进阶知识
装饰器 迭代器 生成器 mixins 元编程 描述符 量化领域常用 列表推导式 字典推导式 高阶函数 lambda函数 三目表达式
- QuantStart量化交易文集
Over the last seven years more than 200 quantitative finance articles have been written by members o ...
- npm install命令对package-lock.json文件自动做了一些额外的更新
今天我使用 npm 命令给项目安装file-saver,通过git却发现package-lock.json中除了file-saver组件之外的其他组件的记录也被改了 npm为何会自动做这些更改呢,又如 ...
- Json与字符串互相转换
jQuery插件支持的转换方式: $.parseJSON( jsonstr ); //jQuery.parseJSON(jsonstr),可以将json字符串转换成json对象 浏览器 ...
- Java-小技巧-004-jdk时间,jdk8时间,joda,calendar,获取当前时间前一周、前一月、前一年的时间
1.推荐使用java8 localdate等 线程安全 支持较好 地址 2.joda 一.简述 查看SampleDateFormat源码,叙述有: * Date formats are not syn ...
- TensorFlow学习笔记(三)-- feed_dict 使用
个人理解:就是TF的一种输入语法. 跟C语言的scanf(),C++的 cin>> 意思差不多,只是长相奇怪了点而已. 做完下面几个例子,基本也就适应了. 首先占位符申请空间:使用的时候, ...
- PAT 1049 Counting Ones [难]
1049 Counting Ones (30 分) The task is simple: given any positive integer N, you are supposed to coun ...