本文系原創(chuàng),轉(zhuǎn)載請(qǐng)注明出處,謝謝 !
前一段時(shí)間因公司業(yè)務(wù)需要,提了這樣一個(gè)需求:要把一個(gè)早期的項(xiàng)目(創(chuàng)建于2013年,非本公司項(xiàng)目)整個(gè)做成SDK,集成到一個(gè)未知的項(xiàng)目里面去,而且還要上線.乍一聽,這個(gè)好像也沒什么難度,由于之前對(duì)靜態(tài)庫略有研究,于是爽快的跟老大說:能做!(當(dāng)時(shí)我還沒拿到源碼,也不知道需求方到底想要如何對(duì)接,對(duì)于主工程更是一無所知).于是接下來兩個(gè)月,本人由于自己的一時(shí)沖動(dòng),入了一個(gè)天坑,差點(diǎn)沒爬出來.現(xiàn)在此項(xiàng)目已完結(jié),在此把自己近2個(gè)月踩過的坑,做個(gè)總結(jié),同時(shí)與大家分享下經(jīng)驗(yàn).
為了更好的梳理思路,我先在此拋出幾個(gè)問題,大家一起思考:
1.把一整個(gè)項(xiàng)目做成SDK,原來工程里面的哪些東西要去掉,
AppDelegate
還能要嗎,.pch
,宏定義
,Categary
這些怎么處理?2.
推送
注簿、分享
之類的功能還能用嗎?3.如果源碼里面用了百度地圖,支付等第三方SDK,能正常的在我們自己的SDK里面調(diào)用并保證功能嗎?(簡(jiǎn)言之,SDK里面能包含SDK嗎)
4.如果源碼內(nèi)容太多,模塊劃分清晰,我們要想按模塊劃分做成多個(gè)SDK,那我們自己做的SDK能互調(diào)嗎?
5.資源文件
(圖片酝静、xib)
怎么處理,源碼里面有近500個(gè),難道要一個(gè)個(gè)修改路徑?6.如果原項(xiàng)目里面用
.strings
,.plist
等一系列的本地文件,路徑該如何加載?7.在對(duì)于主工程一無所知的情況下,
庫的兼容性問題
如何解決?8.即使對(duì)接成功以后,SDK模塊各個(gè)功能顯示正常,可以成功打出
ipa
嗎?9.即使成功的打出了ipa,可以成功的上傳到
AppStore
嗎?10.即使成功的上傳到了AppStore,由于審核人員用iPad測(cè)試項(xiàng)目,如果因?yàn)镾DK模塊中使用了xib加載VC,而xib又
沒有適配iPad
,導(dǎo)致VC界面加載失敗,審核被拒
,怎么辦?......
沒錯(cuò),以上這些問題都遇到了,而且還不止這些.
首先,客觀的說,SDK不是這么用的.
這個(gè)事情本身就是個(gè)變態(tài)的需求.其次SDK對(duì)于源碼是有要求的
,不是隨隨便便給你拉來一套代碼,都能完美的把所有功能做成一個(gè)SDK,然后隨便那個(gè)項(xiàng)目需要,就給哪個(gè)項(xiàng)目調(diào)(這簡(jiǎn)直萬能啊,有木有) 也許有人會(huì)說了,支付寶魔策、百度地圖
不就是這樣嗎?拜托,這些都是功能性
的,沒有哪個(gè)SDK是包含多個(gè)定制化界面和接口
等等一堆東西的.可是,這又怎么樣呢,自己挖的坑,跪著也要填完了.
前言到此結(jié)束,接下來我們言歸正傳.
(以上10個(gè)問題在下面都會(huì)給出答案)
(一) 方案
-
- 何種形式
對(duì)于iOS而言,我的理解:
- 靜態(tài)庫:
.framework
政敢、.a
.framework
=.a
+bundle
, so 我要做出來的,是個(gè)XXX.framework
- 其實(shí)
.framework
本質(zhì)上也是一個(gè)bundle
,只是把資源和二進(jìn)制文件放在一起加載,主工程打包IPA的時(shí)候會(huì)有問題,如上問題8
,所以這種方案我就略過了,免得誤導(dǎo)大家
- 何種形式
- 做成幾個(gè)SDK
一個(gè)! 無論主工程什么樣,暴露一個(gè)接口控制器和幾個(gè)屬性,是最簡(jiǎn)單高效的調(diào)用方法.而且通過本人實(shí)踐證明,我們自己做的SDK不能互調(diào) !!!
同時(shí)回答上面問題4
- 做成幾個(gè)SDK
- 創(chuàng)建一個(gè)
.framework
工程,記得 創(chuàng)建以后Build Setting
--Mach-O Type
選擇Static Lib
- 創(chuàng)建一個(gè)
我創(chuàng)建的 SDK 叫NewCityKit
,測(cè)試SDK的測(cè)試工程為 testFramework
,下文中皆以此為例
(二) 源碼的準(zhǔn)備
-
- 準(zhǔn)備源碼
- 下圖箭頭指向的這些文件統(tǒng)統(tǒng)都不要
(AppDelegate,Assets.xcassets,Base.lproj,main,Resource,info.plist )
- 把項(xiàng)目里面除了這些以外的源文件,添加到你創(chuàng)建的SDK工程里面,
- 另外,需要把項(xiàng)目用到的圖片全部放到一個(gè)文件夾里面(如圖一中的pic),方便
打包成bundle時(shí)選擇源文件
- 建議源碼里面
用到的xib越少越好
,如果像我遇到的這樣,代碼老舊,模塊耦合性高,還非本公司的源碼,對(duì)業(yè)務(wù)邏輯不熟還不給時(shí)間重構(gòu)著急要上線的,那只能硬著頭皮弄了(出坑的關(guān)鍵還在于對(duì)于工作量的準(zhǔn)確評(píng)估和有一個(gè)給力的隊(duì)友)
-
- 在
NewCityKit
里面解決報(bào)錯(cuò)問題,目標(biāo): 編譯通過
- 在此回答
問題1
: AppDelegate ,.pch不能用,宏定義可以,分類也可以,但需要在主工程Build Setting
--Other Linker Flags
添加-Objc灾馒、 -all -load
- 在
-
問題2
:本人認(rèn)為只在SDK里面配置,這些功能實(shí)現(xiàn)的可能性基本為0,除非主工程配合 -
問題3
:可以,本人做項(xiàng)目期間已經(jīng)把百度地圖集成進(jìn)SDK里面,并且可以正常調(diào)用,實(shí)踐證明可行(但是這也說明那些開源成熟的SDK可以被我們自己制作的SDK包含,但是我們自己做的就不行,這個(gè)本人目前還未想通,或許是都可以,還需要更多的嘗試). - 在源碼較多的情況下,要進(jìn)行到編譯通過這一步,還是頗費(fèi)周折的,具體的問題有很多,比如MRC問題啦,.m重復(fù)或者找不到啦等等的,總之
根據(jù)Xcode報(bào)錯(cuò)信息去解決
,基本都沒什么問題的
(三) 資源的處理
-
- 處理資源文件 (本人創(chuàng)建的資源bundle 名為NewCityAsset,下文中都以此為例)
- 在SDK工程里面創(chuàng)建一個(gè)bundle的target,用來存放所有的資源
(圖片、xib)
,具體請(qǐng)參考鏈接xcode 靜態(tài)庫中資源文件及xib打包,關(guān)鍵地方本人也一一截圖出來了,如下:
- 在SDK工程里面創(chuàng)建一個(gè)bundle的target,用來存放所有的資源
* 在此回答`問題5`: 圖片不需要一個(gè)個(gè)修改路徑,但是xib需要
iOS的資源后綴都是@2X,@3X的格式,編譯成為bundle以后,會(huì)變成.tiff格式,這樣你原來寫的路徑圖片名稱后面必須在再上這個(gè)后綴才能取到,這樣就又麻煩了,這個(gè)問題我們可以在資源的target
修改一個(gè)地方,就可以完美解決,如下圖:
- 圖片用以下代碼做處理
寫 一個(gè)加載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寫一個(gè)分類,用運(yùn)行時(shí)替換掉系統(tǒng)的 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 (此種可以用以下代碼來解決,原理同樣是運(yùn)行時(shí)替換系統(tǒng)方法)
#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是以這樣的形式加載的,我們直接就簡(jiǎn)單粗暴,直接把這個(gè)文件拖入宿主工程,什么都不用改,.plist同理
#define mLocalization(key, ...) [NSString stringWithFormat: [[NSBundle mainBundle] localizedStringForKey:key value:@"" table:@"Localization"], ##__VA_ARGS__, nil]
- 但是如果你不想這樣加載(不想向主工程暴露太多東西),你想加載SDK包里面的那個(gè)本地文件,那么,就要像加載圖片那樣,先找到mainBundle,再找到.framework,再找到這個(gè)文件,大體是這么個(gè)路徑
// 注意:二進(jìn)制文件的路徑,從這里找 @"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"];
// 這個(gè)非常關(guān)鍵,千萬不要弄混了!!!
至此,問題6
告一段落
- 然后我們來說一下
用運(yùn)行時(shí)替換系統(tǒng)方法 imageNamed:造成的坑
- 這個(gè)當(dāng)時(shí)和主工程對(duì)接時(shí)直接導(dǎo)致的后果是:由于SDK和主工程都用了MJRefresh,直接造成兩邊的刷新的那個(gè)提示文字變成了英文的,如圖
這個(gè)問題的解決方案也有2個(gè):
- 1.簡(jiǎn)單粗暴的,單獨(dú)把里面的.strings 文件在主工程重新拖一份,就是這個(gè)東西
- 2.我們來看一下MJ 是怎么加載這個(gè)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;
}
我們?cè)賮砜匆幌逻@個(gè)東西最終出現(xiàn)在了哪里
對(duì),是
. framework
!!! 而不是我們自制的.bundle
大家知道怎么做了嗎,原理同上,不再一一贅述至此,bundle的坑基本羅列完畢,在我集成SDK期間,這是出bug最多的地方,也是困擾時(shí)間最長(zhǎng)的,解決的關(guān)鍵就在于找對(duì)路徑,參考經(jīng)典庫,不得不說,MJ對(duì)于bundle的處理容錯(cuò)性還是很高的,值得我們深究一下原理
(四) 庫的兼容性問題
終于進(jìn)行到這個(gè)最大的坑:問題7
了
關(guān)于這個(gè)我又想拋出一個(gè)問題了:
- 比如AFN,SDK和主工程都用了,那最終集成以后,主工程調(diào)用的是它自己的庫還是SDK的庫?
其實(shí)這個(gè)問題我也不太確定,只能根據(jù)實(shí)際猜測(cè)一下,我覺得他是根據(jù)加載順序來的,加載到SDK模塊的時(shí)候,主工程的代碼也優(yōu)先調(diào)用SDK里面的庫(這是庫的版本不一樣的情況下,如果一樣,我也不知道了,反正一樣的話不會(huì)報(bào)錯(cuò))
當(dāng)時(shí)我們遇到這樣一個(gè)問題:
主工程用CocoaPods管理第三方庫,版本都是最新的,而我們的SDK沒有用pod ,庫的版本不詳,不過看樣子像是13年下的,然后,合到一起,就崩到了sessionManager的get方法那里......(unrecognized selector sent to class) 我們的網(wǎng)絡(luò)用的還是AFHTTPRequestOperation,AFHTTPSessionManager的get方法不一樣,怎么辦?- 改AFN方法? 改動(dòng)較大,時(shí)間不夠,隱患略高,而且SDK里面的網(wǎng)絡(luò)請(qǐng)求嵌套了加解密,牽一發(fā)而動(dòng)全身啊,不妥
- 后經(jīng)高人指點(diǎn),找到了一個(gè)完美的解決辦法:修改SDK里面AFHTTPSessionManager的類名和文件名! OC為面向?qū)ο蟮恼Z言,類名改掉了,就不是同一個(gè)對(duì)象了,怎么也調(diào)不到我們的方法了吧,而且基本不影響我們?cè)瓉淼拇a!簡(jiǎn)直完美!!!
最后,關(guān)于這個(gè)問題做個(gè)小結(jié):在SDK和主工程不是同一撥人在臨近時(shí)間段內(nèi)開發(fā)的情況下,除非SDK源碼有時(shí)間重寫或者主工程照著SDK庫的版本來用,否則統(tǒng)一庫的版本幾乎是不可能的,那么這時(shí)解決兼容性問題我覺得可以用上述方法,簡(jiǎn)單而高效.
但是,SDK工程和主工程能否共用一套第三方庫呢,本人沒有嘗試pod是否可以用于SDK工程,如果可以的話,那么這個(gè)猜想是可行的.本人認(rèn)為,這是一個(gè)比較理想的集成SDK的方案,避免了庫版本不統(tǒng)一的問題,同時(shí)大大減小了主工程安裝包的大小,但是這種方案需要在客觀條件允許的情況下,才能實(shí)現(xiàn)
(五) 打包上線的問題
-
1.打包ipa出錯(cuò): Found an unexpected Mach-O header code: 0x72613c21
這個(gè)錯(cuò)誤請(qǐng)參考以下鏈接
iOS 打包 "Found an unexpected Mach-O header code: 0x72613c21"報(bào)錯(cuò)- 原因基本上就是主工程的這個(gè)地方添加了二進(jìn)制文件
(copy bundle resources只能添加資源,不能添加二進(jìn)制文件)
,導(dǎo)致打包失敗
- 原因基本上就是主工程的這個(gè)地方添加了二進(jìn)制文件
2 . upload to AppStore 失敗
item 90171 ,item 90166,
大家可以自行Google,資源NewCityAsset.bundle
里有plist,可執(zhí)行文件(.m,.h,.exe,.o
等等),去掉就好了- 審核被拒
這個(gè)真沒有啥好辦法呢,老老實(shí)實(shí)修改xib(主要是和VC同名的xib),適配iPad,這也是一個(gè)比較坑的地方,如果用的地方太多的話,那就要哭了......
- 審核被拒
最后給大家看一下SDK集成以后的文件目錄:
二進(jìn)制文件和資源分開的,雖然.framework里面包含了.bundle,但是主工程里2個(gè)都要拖進(jìn)來,為了能正常的在主工程添加資源(這個(gè)一定要添加,否則主工程調(diào)用SDK會(huì)出錯(cuò)的)
至此,本人將近2個(gè)月踩過的關(guān)于制作iOS SDK的關(guān)鍵坑,都已經(jīng)羅列完畢!!!
本人絞盡腦汁編寫了將近一天的時(shí)間,如果看完對(duì)你有幫助,請(qǐng)點(diǎn)個(gè)贊!
另外,關(guān)于如何暴露接口控制器,如何調(diào)用傳參等問題,本人覺得比較簡(jiǎn)單,就不細(xì)說了,不會(huì)的同學(xué)請(qǐng)自行搜索,也歡迎大家加我的微信,隨時(shí)交流探討
文章如有錯(cuò)誤之處,歡迎大家批評(píng)指正,謝謝 !