本文系原创,转载请注明出处,谢谢 !
前一段时间因公司业务需要,提了这样一个需求:要把一个早期的项目(创建于2013年,非本公司项目)整个做成SDK,集成到一个未知的项目里面去,而且还要上线.乍一听,这个好像也没什么难度,由于之前对静态库略有研究,于是爽快的跟老大说:能做!(当时我还没拿到源码,也不知道需求方到底想要如何对接,对于主工程更是一无所知).于是接下来两个月,本人由于自己的一时冲动,入了一个天坑,差点没爬出来.现在此项目已完结,在此把自己近2个月踩过的坑,做个总结,同时与大家分享下经验.
为了更好的梳理思路,我先在此抛出几个问题,大家一起思考:
-
1.把一整个项目做成SDK,原来工程里面的哪些东西要去掉,
AppDelegate
还能要吗,.pch
,宏定义
,Categary
这些怎么处理? -
2.
推送
、分享
之类的功能还能用吗? -
3.如果源码里面用了百度地图,支付等第三方SDK,能正常的在我们自己的SDK里面调用并保证功能吗?(简言之,SDK里面能包含SDK吗)
-
4.如果源码内容太多,模块划分清晰,我们要想按模块划分做成多个SDK,那我们自己做的SDK能互调吗?
-
5.资源文件
(图片、xib)
怎么处理,源码里面有近500个,难道要一个个修改路径? -
6.如果原项目里面用
.strings
,.plist
等一系列的本地文件,路径该如何加载? -
7.在对于主工程一无所知的情况下,
库的兼容性问题
如何解决? -
8.即使对接成功以后,SDK模块各个功能显示正常,可以成功打出
ipa
吗? -
9.即使成功的打出了ipa,可以成功的上传到
AppStore
吗? -
10.即使成功的上传到了AppStore,由于审核人员用iPad测试项目,如果因为SDK模块中使用了xib加载VC,而xib又
没有适配iPad
,导致VC界面加载失败,审核被拒
,怎么办? -
......
没错,以上这些问题都遇到了,而且还不止这些.
首先,客观的说,SDK不是这么用的.
这个事情本身就是个变态的需求.其次SDK对于源码是有要求的
,不是随随便便给你拉来一套代码,都能完美的把所有功能做成一个SDK,然后随便那个项目需要,就给哪个项目调(这简直万能啊,有木有) 也许有人会说了,支付宝、百度地图
不就是这样吗?拜托,这些都是功能性
的,没有哪个SDK是包含多个定制化界面和接口
等等一堆东西的.可是,这又怎么样呢,自己挖的坑,跪着也要填完了.
前言到此结束,接下来我们言归正传.
(以上10个问题在下面都会给出答案)
(一) 方案
-
- 何种形式
对于iOS而言,我的理解:
- 静态库:
.framework
、.a
.framework
=.a
+bundle
, so 我要做出来的,是个XXX.framework
- 其实
.framework
本质上也是一个bundle
,只是把资源和二进制文件放在一起加载,主工程打包IPA的时候会有问题,如上问题8
,所以这种方案我就略过了,免得误导大家
- 何种形式
- 做成几个SDK
一个! 无论主工程什么样,暴露一个接口控制器和几个属性,是最简单高效的调用方法.而且通过本人实践证明,我们自己做的SDK不能互调 !!!
同时回答上面问题4
- 做成几个SDK
- 创建一个
.framework
工程,记得 创建以后Build Setting
--Mach-O Type
选择Static Lib
- 创建一个
我创建的 SDK 叫NewCityKit
,测试SDK的测试工程为 testFramework
,下文中皆以此为例
(二) 源码的准备
-
- 准备源码
- 下图箭头指向的这些文件统统都不要
(AppDelegate,Assets.xcassets,Base.lproj,main,Resource,info.plist )
- 把项目里面除了这些以外的源文件,添加到你创建的SDK工程里面,
- 另外,需要把项目用到的图片全部放到一个文件夹里面(如图一中的pic),方便
打包成bundle时选择源文件
- 建议源码里面
用到的xib越少越好
,如果像我遇到的这样,代码老旧,模块耦合性高,还非本公司的源码,对业务逻辑不熟还不给时间重构着急要上线的,那只能硬着头皮弄了(出坑的关键还在于对于工作量的准确评估和有一个给力的队友)
-
- 在
NewCityKit
里面解决报错问题,目标: 编译通过
- 在此回答
问题1
: AppDelegate ,.pch不能用,宏定义可以,分类也可以,但需要在主工程Build Setting
--Other Linker Flags
添加-Objc、 -all -load
- 在
-
问题2
:本人认为只在SDK里面配置,这些功能实现的可能性基本为0,除非主工程配合 -
问题3
:可以,本人做项目期间已经把百度地图集成进SDK里面,并且可以正常调用,实践证明可行(但是这也说明那些开源成熟的SDK可以被我们自己制作的SDK包含,但是我们自己做的就不行,这个本人目前还未想通,或许是都可以,还需要更多的尝试). - 在源码较多的情况下,要进行到编译通过这一步,还是颇费周折的,具体的问题有很多,比如MRC问题啦,.m重复或者找不到啦等等的,总之
根据Xcode报错信息去解决
,基本都没什么问题的
(三) 资源的处理
-
- 处理资源文件 (本人创建的资源bundle 名为NewCityAsset,下文中都以此为例)
- 在SDK工程里面创建一个bundle的target,用来存放所有的资源
(图片、xib)
,具体请参考链接xcode 静态库中资源文件及xib打包,关键地方本人也一一截图出来了,如下:
- 在SDK工程里面创建一个bundle的target,用来存放所有的资源
* 在此回答`问题5`: 图片不需要一个个修改路径,但是xib需要
iOS的资源后缀都是@2X,@3X的格式,编译成为bundle以后,会变成.tiff格式,这样你原来写的路径图片名称后面必须在再上这个后缀才能取到,这样就又麻烦了,这个问题我们可以在资源的target
修改一个地方,就可以完美解决,如下图:
bundle-6.png
- 图片用以下代码做处理
写 一个加载bundle的工具类
#import "BundleTools.h"
#define BUNDLE_NAME @"NewCityAsset"
@implementation BundleTools
+ (NSBundle *)getBundle{
return [NSBundle bundleWithPath: [[NSBundle mainBundle] pathForResource: BUNDLE_NAME ofType: @"bundle"]];
}
+ (NSString *)getBundlePath: (NSString *) assetName{
NSBundle *myBundle = [BundleTools getBundle];
if (myBundle && assetName) {
return [[myBundle resourcePath] stringByAppendingPathComponent: assetName];
}
return nil;
}
给UIImage写一个分类,用运行时替换掉系统的 imageNamed: 方法
#import "UIImage+load.h"
#import "BundleTools.h"
#import <objc/runtime.h>
@implementation UIImage (load)
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
Method m2 = class_getClassMethod([UIImage class], @selector(WB_imageNamed:));
method_exchangeImplementations(m1, m2);
});
}
+ (UIImage *)WB_imageNamed:(NSString *)name{
UIImage *image = [UIImage WB_imageNamed:name];
if (image) {
return image;
}else{
return [UIImage imageNamed:name inBundle:[BundleTools getBundle] compatibleWithTraitCollection:nil];
}
}
至此,图片资源处理完毕.
- xib 分为2种:
- 显示cell的xib(此种没有更好的办法,只能替换
[NSBundle mainBundle] 为 [BundleTools getBundle]
- 显示cell的xib(此种没有更好的办法,只能替换
//替换前
[self.collectionView registerNib:[UINib nibWithNibName:@"CollectionViewCell" bundle: [NSBundle mainBundle]] forCellWithReuseIdentifier:@"CollectionViewCell"];
//替换后
[self.collectionView registerNib:[UINib nibWithNibName:@"CollectionViewCell" bundle:[BundleTools getBundle]] forCellWithReuseIdentifier:@"CollectionViewCell"];
- 显示与VC同名view的xib (此种可以用以下代码来解决,原理同样是运行时替换系统方法)
#import "UIViewController+Bundle.h"
#import "BundleTools.h"
#import <objc/runtime.h>
@implementation UIViewController (Bundle)
+(void)load{
Method m1 = class_getInstanceMethod([self class], @selector(init));
Method m2 = class_getInstanceMethod([self class], @selector(v_init));
method_exchangeImplementations(m1, m2);
}
- (instancetype)v_init {
NSString *path = [[BundleTools getBundle] pathForResource:NSStringFromClass([self class]) ofType:@"nib"];
if (path == nil)
return [self v_init];
else
return [self initWithNibName:NSStringFromClass([self class]) bundle:[BundleTools getBundle]];
}
至此,资源文件处理完毕,但是mainBundle的坑到此还不算完......
- 接着,我们用上面的
问题6
,来引出bundle的坑
先上图本地配置文件:
- 先说 .strings,如果.string是以这样的形式加载的,我们直接就简单粗暴,直接把这个文件拖入宿主工程,什么都不用改,.plist同理
#define mLocalization(key, ...) [NSString stringWithFormat: [[NSBundle mainBundle] localizedStringForKey:key value:@"" table:@"Localization"], ##__VA_ARGS__, nil]
- 但是如果你不想这样加载(不想向主工程暴露太多东西),你想加载SDK包里面的那个本地文件,那么,就要像加载图片那样,先找到mainBundle,再找到.framework,再找到这个文件,大体是这么个路径
// 注意:二进制文件的路径,从这里找 @"NewCityKit.framework"
NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle]pathForResource:@"NewCityKit.framework" ofType:nil]];
NSString* path = [bundle pathForResource:@"APIDecryptConfig" ofType:@"plist"];
// 资源的路径,从这找 @"NewCityAsset.bundle"
NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle]pathForResource:@"NewCityAsset.bundle" ofType:nil]];
NSString* path = [bundle pathForResource:@"new_loading_black" ofType:@"gif"];
// 这个非常关键,千万不要弄混了!!!
至此,问题6
告一段落
- 然后我们来说一下
用运行时替换系统方法 imageNamed:造成的坑
- 这个当时和主工程对接时直接导致的后果是:由于SDK和主工程都用了MJRefresh,直接造成两边的刷新的那个提示文字变成了英文的,如图
这个问题的解决方案也有2个:
- 1.简单粗暴的,单独把里面的.strings 文件在主工程重新拖一份,就是这个东西
- 2.我们来看一下MJ 是怎么加载这个bundle的
+ (instancetype)mj_refreshBundle
{
static NSBundle *refreshBundle = nil;
if (refreshBundle == nil) {
// 这里不使用mainBundle是为了适配pod 1.x和0.x
refreshBundle = [NSBundle bundleWithPath:[[NSBundle bundleForClass:[MJRefreshComponent class]] pathForResource:@"MJRefresh" ofType:@"bundle"]];
}
return refreshBundle;
}
我们再来看一下这个东西最终出现在了哪里
SDK-9.png对,是
. framework
!!! 而不是我们自制的.bundle
大家知道怎么做了吗,原理同上,不再一一赘述至此,bundle的坑基本罗列完毕,在我集成SDK期间,这是出bug最多的地方,也是困扰时间最长的,解决的关键就在于找对路径,参考经典库,不得不说,MJ对于bundle的处理容错性还是很高的,值得我们深究一下原理
(四) 库的兼容性问题
终于进行到这个最大的坑:问题7
了
关于这个我又想抛出一个问题了:
- 比如AFN,SDK和主工程都用了,那最终集成以后,主工程调用的是它自己的库还是SDK的库?
其实这个问题我也不太确定,只能根据实际猜测一下,我觉得他是根据加载顺序来的,加载到SDK模块的时候,主工程的代码也优先调用SDK里面的库(这是库的版本不一样的情况下,如果一样,我也不知道了,反正一样的话不会报错)
当时我们遇到这样一个问题:
主工程用CocoaPods管理第三方库,版本都是最新的,而我们的SDK没有用pod ,库的版本不详,不过看样子像是13年下的,然后,合到一起,就崩到了sessionManager的get方法那里......(unrecognized selector sent to class) 我们的网络用的还是AFHTTPRequestOperation,AFHTTPSessionManager的get方法不一样,怎么办?- 改AFN方法? 改动较大,时间不够,隐患略高,而且SDK里面的网络请求嵌套了加解密,牵一发而动全身啊,不妥
- 后经高人指点,找到了一个完美的解决办法:修改SDK里面AFHTTPSessionManager的类名和文件名! OC为面向对象的语言,类名改掉了,就不是同一个对象了,怎么也调不到我们的方法了吧,而且基本不影响我们原来的代码!简直完美!!!
最后,关于这个问题做个小结:在SDK和主工程不是同一拨人在临近时间段内开发的情况下,除非SDK源码有时间重写或者主工程照着SDK库的版本来用,否则统一库的版本几乎是不可能的,那么这时解决兼容性问题我觉得可以用上述方法,简单而高效.
但是,SDK工程和主工程能否共用一套第三方库呢,本人没有尝试pod是否可以用于SDK工程,如果可以的话,那么这个猜想是可行的.本人认为,这是一个比较理想的集成SDK的方案,避免了库版本不统一的问题,同时大大减小了主工程安装包的大小,但是这种方案需要在客观条件允许的情况下,才能实现
(五) 打包上线的问题
-
1.打包ipa出错: Found an unexpected Mach-O header code: 0x72613c21
这个错误请参考以下链接
iOS 打包 "Found an unexpected Mach-O header code: 0x72613c21"报错- 原因基本上就是主工程的这个地方添加了二进制文件
(copy bundle resources只能添加资源,不能添加二进制文件)
,导致打包失败
- 原因基本上就是主工程的这个地方添加了二进制文件
-
2 . upload to AppStore 失败
item 90171 ,item 90166,
大家可以自行Google,资源NewCityAsset.bundle
里有plist,可执行文件(.m,.h,.exe,.o
等等),去掉就好了 - 审核被拒
这个真没有啥好办法呢,老老实实修改xib(主要是和VC同名的xib),适配iPad,这也是一个比较坑的地方,如果用的地方太多的话,那就要哭了......
- 审核被拒
最后给大家看一下SDK集成以后的文件目录:
SDK-11.png二进制文件和资源分开的,虽然.framework里面包含了.bundle,但是主工程里2个都要拖进来,为了能正常的在主工程添加资源(这个一定要添加,否则主工程调用SDK会出错的)
SDK-13.png至此,本人将近2个月踩过的关于制作iOS SDK的关键坑,都已经罗列完毕!!!
本人绞尽脑汁编写了将近一天的时间,如果看完对你有帮助,请点个赞!
另外,关于如何暴露接口控制器,如何调用传参等问题,本人觉得比较简单,就不细说了,不会的同学请自行搜索,也欢迎大家加我的微信,随时交流探讨
文章如有错误之处,欢迎大家批评指正,谢谢 !
网友评论