引言
最近在做SDK,我们的SDK中需要集成很多第三方库,比如Twitter、Facebook等,我们SDK中需要用到这些第三方SDK中的登录、分享等功能,这些第三方库我们不会打包到我们自己的SDK中,而是接入方通过Pod或者其他方式导入到他们的工程。接我们SDK的人需求是不一样的,有的只用到Facebook、有的只用到Twitter。如果有一个接入我们SDK的人只用到Facebook,但是我们要求接入者不仅要导入Facebook的SDK,还要导入Twitter的SDK(因为SDK中集成了Twitter的东西,不导入会报错),这样是不是不太好,而且接入者可能会说,我们都没有用到这个第三方,为什么要导入他们的SDK。
那么问题来了,我们要怎么样才能做到我们SDK中既集成所有的第三方SDK,又能满足接入者用到哪个第三方才导入对应的第三方SDK?
解决方案
经过一段时间的琢磨、想出了几个方案:
方案一:在SDK内部判断是否导入了某个第三方库,然后通过预编译的方式,是否要编译某段代码(这个方案行不通)
方案二:SDK内部通过runtime的方式调用第三方SDK的东西,通过动态的方式使用类,方法及属性(这个方法可以)
方案三:SDK 分成两个部分,一个部分是Framework部分,一个部分是源码部分,结合方案一和方案二(最终的方案)
下面详细的说说这三种方案,包括这三种方案使用时遇到的困难以及优缺点
注意:本文中的代码只是为了简单的测试,在正常开发的时候,最好进行一些相应的判断,以免引起不必要的麻烦
方案一
直接上代码,这是.m文件所有示例代码
#import "Test.h"
// 判断是否存在TwitterKit/TWTRKit.h,如果存在说明导入了Twitter的SDK
#define TwitterSDKModule __has_include(<TwitterKit/TWTRKit.h>)
// 在导入了Twitter的SDK情况下,才引入
#if TwitterSDKModule
@import TwitterKit;
#endif
// 遵循Twitter的某个协议,以及使用Twitter的某个类的对象最为属性
#if TwitterSDKModule
@interface Test()<这儿是Twitter的某个协议>
@property (nonatomic, strong) TWTRTwitter *twitter;
@end
#endif
// 跟Twitter没关系的属性
@interface Test()
@property (nonatomic, copy) NSString *message;
@end
@implementation Test
#if TwitterSDKModule
/// 调用Twitter的方法
- (void)loginByTwitter {
[[Twitter sharedInstance] logInWithCompletion:^(TWTRSession * _Nullable session, NSError * _Nullable error) {
}];
}
#endif
@end
原本信心满满,但是惨惨招打脸,为什么这种方式不行呢?原因就在于我们打包Framework时,我们SDK内部的代码就编译好了,是否存在<TwitterKit/TWTRKit.h>在打包Framework时就决定了,而不是取决于接入者的工程中是否包含<TwitterKit/TWTRKit.h>。所以这种方式宣告失败
方案二
使用runtime方式。我们可以通过runtime的方式,在不引入头文件的情况下进行某个类的调用
具体参考代码如下:
// 获取Person类
Class personClass = NSClassFromString(@"Person");
// 获取方法编号
SEL shareSEL = NSSelectorFromString(@"shared");
// 调用Person类的类方法
[personClass performSelector:shareSEL withObject:nil];
// 创建实例化对象
id person = [[personClass alloc] init];
// 获取对象的属性
NSString *name = [person valueForKey:@"name"];
// 给属性赋值
[person setValue:@"xiao hai" forKey:@"name"];
// 获取方法编号
SEL runSEL = NSSelectorFromString(@"run");
// 实例方法的调用
[person performSelector:runSEL withObject:nil];
这里获取类、调用类方法、获取属性值、给属性赋值、调用实例方法都有了,那么问题来了,我们要怎么去遵循协议、实现协议的方法,常规写法如下:
@interface Test ()<PersonDelegate>
@end
@implementation ViewController
- (void)eatWithPerson:(Person *)person {
}
@end
那么问题来了,如果不引入头文件的情况下,PersonDelegate、Person都是未知的,代理方法里还有很多未知的类,我们应该如何解决,首先“@interface Test ()<PersonDelegate>”中, PersonDelegate代理完全不用写,之所以要写,我个人理解是为了写代码方便,减少写代码带来的错误
遵循代理解决了,那么代理方法的实现怎么解决,Person是未知的,直接写上去肯定会报错,我们只需要将Person用id代替就行了,之所以可以这么写,是因为这两种写法,方法名是一样的,都是“eatWithPerson:”,这样就解决了代理相关问题,简直perfect,实例代码如下:
@interface Test ()
@end
@implementation Test
- (void)eatWithPerson:(id)person {
}
@end
本以为问题都解决了,但是又遇到问题了,performSelector方法不支持多参数,于是苦苦寻找解决方法,终于在GitHub上找到了解决办法,MXRuntimeUtils帮我解决了这个问题,非常感谢MXRuntimeUtils的作者。本应该再次高兴时,问题又又又出现了,MXRuntimeUtils不支持block类型的参数,于是我对MXRuntimeUtils做了改进,增加了对block类型参数的支持,如果有需要的小伙伴可以联系我
这种方式终于结束了,下面我说说这种方式很大的一个弊端,每个使用第三方SDK的地方都要用这种方式来写,我只能说太累,太麻烦了
方案三(最终方案)
把方案一和方案二结合,并进行改进。我以SDK集成Facebook、Twitter为例子,下面我直接给出我们SDK在这块的架构示意图

我针对这个图解释一下,整个SDK分成两个部分,Framework部分和源文件部分,Framework部分是我们常规把包达成Framework的形式,源文件部分,主要是对第三方SDK的接口进行二次封装,直接向SDK接入者暴露源代码(完全不牵扯到我们SDK内部业务,代码暴露给接入者没什么关系)。源文件中的Adapter(下面简称A)是对需要的第三方接口进行整合,比如我们SDK需要集成Twitter和Facebook的登录功能,A中就提供一个登录接口(通过一个参数来判断要调用Twitter的登录还是Facebook的登录)源文件中的对Twitter、Facebook接口的二次封装,通过方案一中使用的方法封装。Framework中的Adapter(下面简称B)是对A进行翻译,供Framework部分中其他地方直接调用。B是framework内部文件,A是Framework外部文件,B如何调用A呢,可以通过方案二中的runtime形式调用。
分析:这种方案也用到了跟方案一中几乎一样的方案,为什么方案一达不到效果,而这种方案可以,原因在于在这个方案中,把方案一中用到的东西放到了源代码中。这两种方案,区别在于我们写的有关预编译的部分的编译时机,方案一,预编译部分的代码在打包Framework的时候就编译好了,而这种方案,预编译部分的代码在接入方编译时才编译,达到了我们想要的效果。
结束语
如果有什么写的不对的地方,欢迎指正,如果有什么不明白的地方也很欢迎一起探讨
作者联系方式:QQ 782304472 (要加的小伙伴请写明在此文章中看到的,谢谢)
网友评论