此文章由热心网友 ccSundayChina授权转载
接上篇文章,里面主要讲述了普通链式文件与分类链式文件的生成过程。今天讲一下里面的几个技术难点。
一:为什么要构造链式方法宏定义以及如何构造。
思考一个问题,当我们给类添加了链式方法后,比如- (MLChain4UIButton *(^)())addTarget_action_forControlEvents;
其实我们就已经可以进行调用了,比如这样
UIButton.mlc_make. addTarget_action_forEvents(self, @selector(buttonClicked:), UIControlEventTouchUpInside)
这样写是不会报错的,但是因为我们没有给这个链式方法进行相应的实现,所以它是不会产生任何效果的。而如果亲自动手给每一个链式方法都添加实现的话,就偏离了我们的初衷。
仔细想一想,其实它的实现跟原生类里面的方法addTarget:(nullable id) action:(nonnull SEL) forControlEvents:(UIControlEvents)
的实现是一模一样的,所以如果我们在调用链式方法的时候能够让这个链式对象所对应的原生对象去响应它的原生方法的话,那么就能实现我们想要的效果了,而这时候我们也不需要写任何的实现代码。
那么怎么实现这个功能呢,首先我们要知道的是,链式方法名与原生方法名是可以按照固定的规则进行相互的转换的,而链式对象与原生对象也是可以按照固定规则进行相互的转换。也就是说知道了一个就可以推算出另一个。这也是代码中NSObject+ChainInfoAdaptor分类所做的事情,它像是一个适配器一样,可以根据你的需要将链式方法名转换成原生方法名等,反之依然。而如果我们知道了原生的类,原生的方法,以及方法的参数
的话,那么我们就可以根据NSInvocation逐一根据数据类型设置,最后使用[Invocation invoke]来手动的触发这个方法。
所以我们在调用链式方法的时候,希望可以将这个方法所对应的原生类、原生方法、以及它的各个参数都传递出去,让另一个专门的类(NSObject+ChainInvocation
)对这些进行统一处理,最后实现原生对象调用原生方法的效果。
这时候仅仅通过一个链式方法是无法实现的,因此需要一个更巧妙的设计。
我们知道如果定义一个形如addTarget_action_forControlEvents(...)
跟链式方法同名的的宏定义的话,那么我们再写上面的UIButton.mlc_make. addTarget_action_forEvents(self, @selector(buttonClicked:), UIControlEventTouchUpInside)
的时候,程序就会进到那个宏里面去,这是我们必须清楚地一件事情。
知道了这个前提,我们就可以在这上面的宏中做文章了。思考一下如何使用这个宏传出去原生方法、方法的参数(个数不确定)和原生的类(这个可以在外面再获取),由于原生方法在生成链式方法的时候是已经知道了的,因为我们是通过原生方法映射出链式方法的,所以我们可以在自动生成文件的时候就将原生方法名作为宏中已知的默认第一项,而外界输入的可变参数作为第二个大项(可以通过RAC中的宏定义metamacro_at(N, ...)
来依次获取参数的值)对外进行传递。所以我们需要的宏定义如下:
#define addTarget_action_forControlEvents(...) addTarget_action_forControlEvents(@"addTarget:action:forControlEvents:", metamacro_at(0, __VA_ARGS__), metamacro_at(1, __VA_ARGS__), (long long)metamacro_at(2, __VA_ARGS__))
因此我们在自动生成宏定义的时候围绕上面的原则进行生成就可以了,需要在注意一下参数的类型。关键代码如下:
macroDefineString = [NSString stringWithFormat: @"#ifndef %@\
\n#define %@(...) %@(@\"%@\", %@)\
\n#endif", chainSelName, chainSelName, chainSelName, selName, [mulArgStrs componentsJoinedByString:@", "]];
//chainSelName:链式方法名,selName:原生方法名,mulArgStrs:该方法的参数类型数组。
二:如何在调用链式方法的时候,转为让原生的对象去响应原生的方法。
定义好了统一的链式宏定义后,我们希望能够将宏中的那些条件都传到一个固定的方法中进行处理。也就是说在调用链式方法的时候,无论是什么,它的实现都能够进到另一个固定的方法中。
为此我们在每一个类中的+(void) load;
方法(这个方法的执行在main函数前)中给每一个链式方法都进行了动态的绑定,并将它们方法的实现转到一个固定的方法中。
+ (void)mlc_setUpMethodDynamically{
Class originalClass = MLCOriginalClass(self);//获取链式类所对应的原生类
Class chainBridgeClass = self; //当前链式类
Method desMethod = class_getInstanceMethod([originalClass class], @selector(mlc_rootChainMethod)); //mlc_rootChainMethod是我们统一处理所有链式方法的类,所有链式方法都会走这个方法。
IMP imp = method_getImplementation(desMethod);//获取mlc_rootChainMethod的实现
for (NSString *methodName in [originalClass mlc_noReturnValueSelNames]) {
NSString *chainMethodName = [originalClass mlc_chainSelNameWithOringalSelName:methodName];//将原生方法名转换为链式方法名
class_addMethod([chainBridgeClass class], sel_registerName(chainMethodName.UTF8String), imp, method_getTypeEncoding(desMethod));//给链式类动态添加链式方法,并且将mlc_rootChainMethod方法的实现作为它们的统一实现。
}
}
下面看一下所有链式方法的最终实现:mlc_rootChainMethod
方法。这是在NSObject+ChainInvocation分类中定义的方法。
- (instancetype (^)(NSString *selName, ...))mlc_rootChainMethod{
return ^ id (NSString *selName, ...){
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
id chainObject = [self performSelector:@selector(chainObject)];
#pragma clang diagnostic pop
NSMethodSignature *sig = [chainObject methodSignatureForSelector:sel_registerName(selName.UTF8String)];
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
[inv setTarget:chainObject];
[inv setSelector:sel_registerName(selName.UTF8String)];
va_list args;
va_start(args, selName);
[NSObject mlc_setInv:inv withSig:sig andArgs:args];
va_end(args);
[inv invoke];
return self;
};
}
在这里这个方法的block参数第一项就是原生类的原始方法,而后面接收一个可变参数的列表,也就是我们从宏定义中获取到的参数。
根据这些条件,我们就可以让原生类调用原生的方法了。
三:再梳理一下程序的流程
1、在生成文件的时候,让链式方法名与链式宏定义的名字一样。并在宏定义中将原生方法名作为第一项默认参数,输入的方法变量作为可变参数列表这个第二个大项。链式宏及方法如下:
#define chainSelName(...) chainSelName(@"originalSelName", [mulArgStrs componentsJoinedByString:@", "]])
//chainSelName:链式方法名、originalSelName:原生方法、[mulArgStrs componentsJoinedByString:@", "]]方法参数类型宏定义。
- (MLChain4Object *(^)()) chainSelName;
2、程序启动时,给所有的链式类的链式方法做一个动态的绑定,并且将分类中的mlc_rootChainMethod
方法作为他们的实现。
这样在书写形如UIButton.mlc_make. addTarget_action_forEvents(self, @selector(buttonClicked:), UIControlEventTouchUpInside)
的时候,就会进到- (instancetype (^)(NSString *selName, ...))mlc_rootChainMethod方法中,在这里我们会获取到原生类、原生方法、参数列表,获取到这些我们就可以手动的触发方法了。主要就是使用NSInvocation逐一根据数据类型设置,设置的方法setInv:withSig:andArgs:从YYKit中得来。篇幅有限,这里就先不做过多的对这个方法的阐述了,之后会加入的。