• Objective-C 方法交换实践(二)


    一. 基本函数

    1. 根据 sel 得到 class 的实例方法
    	Method class_getInstanceMethod(Class cls, SEL name)
    
    1. 根据 sel 得到 class 的函数指针
    	IMP class_getMethodImplementation(Class cls, SEL name)
    
    1. 给 class 添加方法
    	class_addMethod(Class cls, SEL name, IMP imp, const char * types) 
    
    1. 替换 class 的 sel 对应的函数指针,返回值为 sel 对应的原函数指针
    	class_replaceMethod(Class cls, SEL name, IMP imp, const char * types) 
    
    1. 交换两个 method
    	method_exchangeImplementations(Method m1, Method m2)
    
    1. 直接替换 method 的函数指针
    	method_setImplementation(Method method, IMP imp)
    

    二. 主要问题

    1. 原子性操作问题

    解决方案一般是在 `+(void)load`方法中处理;也可以加锁;还可以在`+(void)initialize`中去做,但是一定要注意继承的问题。
    

    2. 改变范围超出预期

    比如你可能会只想修改一个实例的方法,但实际上你修改了所有的实例方法。比如你交换的方法真实的实现是在父类中的,你的修改会影响所有的父类派生出来的类。
    例如,直接使用 `method_exchangeImplementations` 方法,考虑下这种情况
    
    	@ B
    	- (void)case1
    	{
    	    NSLog(@"case 1 B");
    	}
    	@end
    	
    	@interface C: B
    	
    	@property (nonatomic, copy) NSString *x;
    	
    	@end
    	
    	@implementation C
    	- (void)case2
    	{
    	    NSLog(@"case2 C %@-%@",[self class],self.x);
    	}
    	
    	@end
    	
    	- (void)someMethod {
    	    Method a1 = class_getInstanceMethod([C class], @selector(case1));
    	    Method a2 = class_getInstanceMethod([C class], @selector(case2));
    	    method_exchangeImplementations(a1, a2);
    	    
    	    B *b = [[B alloc] init];
    	    [b case1];
    	}
    

    会发生什么呢?会 crash ,因为 C 作为 B 的子类并没有实现 case1 方法,方法交换会把 B 的case1 替换成 C 的 case2,后面 [b case1]  其实会执行 void _.._case2(C * self, SEL _cmd) 这个函数,里面调用 x 属性,所以 crash。

    为了避免这个错误,一般的做法有,先用 class_addMethod 判断能否添加将要替换的方法,如果可以添加,说明子类原先没有实现此方法,这个方法是在父类中实现的。具体可以看参考1。

    RSSwizzlejrswizzle 都避免了这个问题。

    3. 可能有命名冲突

    比如你交换的方法很可能在别的地方(比如类别里)已经有同样命名的存在了。此时的避免方法可以是直接去替换 Method 里的函数指针,保存原有的函数指针来调用:
    
    		typedef IMP *IMPPointer;
    		
    		BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    		    IMP imp = NULL;
    		    Method method = class_getInstanceMethod(class, original);
    		    if (method) {
    		        const char *type = method_getTypeEncoding(method);
    		        imp = class_replaceMethod(class, original, replacement, type);
    		        if (!imp) {
    		            imp = method_getImplementation(method);
    		        }
    		    }
    		    if (imp && store) { *store = imp; }
    		    return (imp != NULL);
    		}
    		
    		@implementation NSObject (FRRuntimeAdditions)
    		+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    		    return class_swizzleMethodAndStore(self, original, replacement, store);
    		}
    		@end
    

    4. 可能会使用不一样的方法参数

    比如同样调用原来的函数时,`_cmd`已经不一样了,解决方案可以和上面一致。
    

    5. 类簇类的swizzling

    对于 Objective-C 中的一些类簇类,比如 NSNumber、NSArray和NSMutableArray 等,因为这些并不是一个具体的类,而是一个抽象类,如果直接在这些类的内部写个方法通过self class等方式来获取 Class 并做方法交换的话,因为并不能获得其真实的类名,所以会达不到想要的效果。

    比如,我们可以通过以下代码来得到NSMutableArray的真实类型:

    	object_getClass([[NSMutableArray alloc] init]);
    	objc_getClass("__NSArrayM");
    

    上面代码中__NSArrayMNSMutableArray的真实类名;

    6. 子类方法调用了 super 方法,并且都做了交换

    比如下面的例子就会发生循环调用。
    
    	@ A
    	- (void)log {
    	    NSLog(@"i am a");
    	}
    	
    	- (void)print {
    	    [self print];
    	}
    	@end
    	
    	@ B
    	- (void)log {
    	    NSLog(@"i am b");
    	    [super log];
    	}
    	
    	- (void)print {
    	    [self print];
    	}
    	@end
    

    下面做一下方法交换,并执行子类的方法。

    	- (void)test {
    	    Method a1 = class_getInstanceMethod([A class], @selector(log));
    	    Method a2 = class_getInstanceMethod([A class], @selector(print));
    	    method_exchangeImplementations(a1, a2);
    	
    	    Method a3 = class_getInstanceMethod([B class], @selector(log));
    	    Method a4 = class_getInstanceMethod([B class], @selector(print));
    	    method_exchangeImplementations(a3, a4);
    	    
    	    B *b = [[B alloc] init];
    	    [b print];
    	}
    

    方法的调用流程(用imp来表示)

    	B.log - A.print - B.log....
    

    从而形成了循环的引用。

    三. 方法交换的实现

    1. 直接修改 Method 的函数指针

    参考2中提到的,利用 (一、1)中的方法,额外提供一个变量来存储原始的函数指针,如果需要调用原始方法,就用这个变量来主动设置 sel 参数来防止原始函数用到了_cmd 的情况

    2. jrswizzle

    主要用到了 method_exchangeImplementations 方法,鲁棒性上做的工作就是先做了 class_addMethod 操作。简单是很简单,然而上面所说的大部分问题他都不能避免。

    3. RSSwizzle

    主要用到了 class_replaceMethod 方法,避免了子类的替换影响了父类。而且对方法交换加了锁,增强了线程安全。有更多的替换选项。并且,他通过block引入了两个方法互相调用或者子类父类同时交换导致的循环问题。上面的问题几乎都可以避免。
    问题是:OSSpinLock 不被建议使用了。
    官方文档说他解决了method_exchangeImplementations 的限制:

    1. 只有在 +load 方法中才线程安全
    2. 对没有重载的方法交换会遇到非期望的结果
    3. 交换的方法不能依赖 _cmd 参数 (通过 RSSwizzleInfo结构,保存原始的 selector)
    4. 命名冲突

    参考:
    1.http://nshipster.cn/method-swizzling/
    2.https://blog.newrelic.com/2014/04/16/right-way-to-swizzle/
    3.http://yulingtianxia.com/blog/2017/04/17/Objective-C-Method-Swizzling/
    4.https://stackoverflow.com/questions/5339276/what-are-the-dangers-of-method-swizzling-in-objective-c

  • 相关阅读:
    (C#) 设定时间格式
    (WPF) MVVM: 动态添加控件及绑定。
    (WPF) MVVM: DataGrid Binding
    (WPF) MVVM: ComboBox Binding, XML 序列化
    (C#) 判断相等?
    ASP.NET MVC过滤器中权限过滤器ValidateAntiForgeryToken的用法(Post-Only)
    根据2个经纬度点,计算这2个经纬度点之间的距离
    ASP.NET导出Excel(利用NPOI和EPPlus库,无需安装Office)
    nopcommerce 二次开发
    SQL效率低下原因主要有
  • 原文地址:https://www.cnblogs.com/v2m_/p/7869025.html
Copyright © 2020-2023  润新知