前言
目前 iOS 上的動態(tài)更新方案主要有以下 4 種:
HTML 5
lua(wax)hotpatch
react native
framework
前面三種都是通過在應(yīng)用內(nèi)搭建一個運行環(huán)境來實現(xiàn)動態(tài)更新(HTML 5 是原生支持)育谬,在用戶體驗捧毛、與系統(tǒng)交互上有一定的限制箱锐,對開發(fā)者的要求也更高(至少得熟悉 lua 或者 js)距潘。
使用 framework 的方式來更新可以不依賴第三方庫屹堰,使用原生的 OC/Swift 來開發(fā),體驗更好坤邪,開發(fā)成本也更低挪捕。
由于 Apple 不希望開發(fā)者繞過 App Store 來更新 app,因此?只有對于不需要上架的應(yīng)用,才能以 framework 的方式實現(xiàn) app 的更新浮毯。
主要思路
將 app 中的某個模塊(比如一個 tab)的內(nèi)容獨立成一個 framework 的形式動態(tài)加載完疫,在 app 的 main bundle 中,當 app 啟動時從服務(wù)器上下載新版本的 framework 并加載即可達到動態(tài)更新的目的亲轨。代碼放在了這里趋惨。
實戰(zhàn)
創(chuàng)建一個普通工程 DynamicUpdateDemo,其包含一個 framework 子工程 Module惦蚊。也可以將 Module 創(chuàng)建為獨立的工程器虾,創(chuàng)建工程的過程不再贅述。
依賴
在主工程的 Build Phases > Target Dependencies 中添加 Module蹦锋,并且添加一個 New Copy Files Phase兆沙。
這樣,打包時會將生成的 Module.framework 添加到 main bundle 的根目錄下莉掂。
加載
主要的代碼如下:
- (UIViewController *)loadFrameworkNamed:(NSString *)bundleName {? ? NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);? ? NSString *documentDirectory = nil;if ([paths count] !=0) {? ? ? ? documentDirectory = [paths objectAtIndex:0];? ? }? ? ? ? NSFileManager *manager = [NSFileManager defaultManager];? ? NSString *bundlePath = [documentDirectory stringByAppendingPathComponent:[bundleName stringByAppendingString:@".framework"]];// Check if new bundle existsif (![manager fileExistsAtPath:bundlePath]) {? ? ? ? NSLog(@"No framework update");? ? ? ? bundlePath = [[NSBundle mainBundle]? ? ? ? ? ? ? ? ? ? ? pathForResource:bundleName ofType:@"framework"];// Check if default bundle existsif (![manager fileExistsAtPath:bundlePath]) {? ? ? ? ? ? UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Oooops" message:@"Framework not found"delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil];? ? ? ? ? ? [alertView show];return nil;? ? ? ? }? ? }// Load bundle? ? NSError *error = nil;? ? NSBundle *frameworkBundle = [NSBundle bundleWithPath:bundlePath];if (frameworkBundle && [frameworkBundle loadAndReturnError:&error]) {? ? ? ? NSLog(@"Load framework successfully");? ? }else {? ? ? ? NSLog(@"Failed to load framework with err: %@",error);return nil;? ? }// Load class? ? Class PublicAPIClass = NSClassFromString(@"PublicAPI");if (!PublicAPIClass) {? ? ? ? NSLog(@"Unable to load class");return nil;? ? }? ? ? ? NSObject *publicAPIObject = [PublicAPIClassnew];return [publicAPIObject performSelector:@selector(mainViewController)];
}
代碼先嘗試在 Document 目錄下尋找更新后的 framework葛圃,如果沒有找到,再在 main bundle 中尋找默認的 framework憎妙。
其中的關(guān)鍵是利用 OC 的動態(tài)特性NSClassFromString和performSelector加載 framework 的類并且執(zhí)行其方法库正。
framework 和 host 工程資源共用
第三方庫
ClassXXXisimplementedinbothXXXandXXX.Oneofthetwowillbeused.Whichoneisundefined.
這是當 framework 工程和 host 工程鏈接了相同的第三方庫或者類造成的。
為了讓打出的 framework 中不包含 host 工程中已包含的三方庫(如 cocoapods 工程編譯出的 .a 文件)厘唾,可以這樣:
刪除Build Phases > Link Binary With Libraries中的內(nèi)容(如有)褥符。此時編譯會提示三方庫中包含的符號找不到。
在 framework 的Build Settings > Other Linker Flags添加-undefined dynamic_lookup抚垃。?必須保證 host 工程編譯出的二進制文件中包含這些符號喷楣。
類文件
嘗試過在 framework 中引用 host 工程中已有的文件,通過Build Settings > Header Search Paths中添加相應(yīng)的目錄鹤树,Xcode 在編譯的時候可以成功(因為添加了-undefined dynamic_lookup)铣焊,并且 Debug 版本是可以正常運行的,但是 Release 版本動態(tài)加載時會提示找不到符號:
Error Domain=NSCocoaErrorDomain Code=3588"The bundle “YourFramework” couldn’t be loaded." (dlopen(/var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework,265): Symbol not found: _OBJC_CLASS_$_BorderedViewReferencedfrom: /var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFrameworkExpectedin: flatnamespacein /var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework) UserInfo=0x174276900 {NSLocalizedFailureReason=The bundle couldn’t be loaded., NSLocalizedRecoverySuggestion=Try reinstalling the bundle., NSFilePath=/var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework, NSDebugDescription=dlopen(/var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework,265): Symbol not found: _OBJC_CLASS_$_BorderedViewReferencedfrom: /var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFrameworkExpectedin: flatnamespacein /var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework, NSBundlePath=/var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework, NSLocalizedDescription=The bundle “YourFramework” couldn’t be loaded.}
因為 Debug 版本暴露了所有自定義類的符號以便于調(diào)試罕伯,因此你的 framework 可以找到相應(yīng)的符號曲伊,而 Release 版本則不會。
目前能想到的方法只有將相同的文件拷貝一份到 framework 工程里追他,并且更改類名熊昌。
訪問 framework 中的圖片
在 storyboard/xib 中可以直接訪問圖片,代碼中訪問的方法如下:
UIImage*image = [UIImage imageNamed:@"YourFramework.framework/imageName"]
注意:使用代碼方式訪問的圖片不可以放在 xcassets 中湿酸,否則得到的將是 nil。并且文件名必須以 @2x/@3x 結(jié)尾灭美,大小寫敏感推溃。因為imageNamed:默認在 main bundle 中查找圖片。
常見錯誤
Architecture
dlopen(/path/to/framework,9): no suitable image found.? Did find:/path/to/framework: mach-o, but wrong architecture
這是說 framework 不支持當前機器的架構(gòu)届腐。通過
lipo -info /path/to/MyFramework.framework/MyFramework
可以查看 framework 支持的 CPU 架構(gòu)铁坎。
碰到這種錯誤蜂奸,一般是因為編譯 framework 的時候,scheme 選擇的是模擬器硬萍,應(yīng)該選擇iOS Device扩所。
此外,如果沒有選擇iOS Device朴乖,編譯完成后祖屏,Products 目錄下的 .framework 文件名會一直是紅色,只有在 Derived Data 目錄下才能找到編譯生成的 .framework 文件买羞。
簽名
系統(tǒng)在加載動態(tài)庫時袁勺,會檢查 framework 的簽名,簽名中必須包含 TeamIdentifier 并且 framework 和 host app 的 TeamIdentifier 必須一致畜普。
如果不一致期丰,否則會報下面的錯誤:
Error loading /path/to/framework: dlopen(/path/to/framework,265): no suitable image found. Did find:/path/to/framework: mmap() error1
此外,如果用來打包的證書是 iOS 8 發(fā)布之前生成的吃挑,則打出的包驗證的時候會沒有 TeamIdentifier 這一項钝荡。這時在加載 framework 的時候會報下面的錯誤:
[deny-mmap] mapped file has no team identifier andis not a platform binary:/private/var/mobile/Containers/Bundle/Application/5D8FB2F7-1083-4564-94B2-0CB7DC75C9D1/YourAppNameHere.app/Frameworks/YourFramework.framework/YourFramework
可以通過codesign命令來驗證。
codesign -dv /path/to/YourApp.app
如果證書太舊舶衬,輸出的結(jié)果如下:
Executable=/path/to/YourApp.app/YourAppIdentifier=com.company.yourappFormat=bundle with Mach-O thin (armv7)CodeDirectory v=20100 size=221748 flags=0x0(none) hashes=11079+5 location=embeddedSignature size=4321Signed Time=2015年10月21日 上午10:18:37Info.plist entries=42TeamIdentifier=notsetSealed Resources version=2 rules=12 files=2451Internal requirementscount=1size=188
注意其中的TeamIdentifier=not set埠通。
采用 swift 加載 libswiftCore.dylib 這個動態(tài)庫的時候也會遇到這個問題,對此Apple 官方的解釋是:
To correct this problem, you will need to sign your app using code signing certificates with the Subject Organizational Unit (OU) set to your Team ID. All Enterprise and standard iOS developer certificates that are created after iOS 8 was released have the new Team ID field in the proper place to allow Swift language apps to run.
If you are an in-house Enterprise developer you will need to be careful that you do not revoke a distribution certificate that was used to sign an app any one of your Enterprise employees is still using as any apps that were signed with that enterprise distribution certificate will stop working immediately.
只能通過重新生成證書來解決這個問題约炎。但是 revoke 舊的證書會使所有用戶已經(jīng)安裝的植阴,用該證書打包的 app 無法運行。
等等圾浅,我們就跪在這里了嗎掠手?!
現(xiàn)在企業(yè)證書的有效期是三年狸捕,當證書過期時喷鸽,其打包的應(yīng)用就不能運行,那企業(yè)應(yīng)用怎么來更替證書呢灸拍?
Apple 為每個賬號提供了兩個證書做祝,這兩個證書可以同時生效,這樣在正在使用的證書過期之前鸡岗,可以使用另外一個證書打包發(fā)布混槐,讓用戶升級到新版本鹰霍。
也就是說贾费,可以使用另外一個證書來打包應(yīng)用似踱,并且可以覆蓋安裝使用舊證書打包的應(yīng)用。詳情可以看Apple 文檔次询。
You are responsible for managing your team’s certificates and provisioning profiles. Apple Developer Enterprise Program certificates expire after three years and provisioning profiles expire after one year.
Before a distribution certificate expires, create an additional distribution certificate, described in Creating Additional Enterprise Distribution Certificates. You cannot renew an expired certificate. Instead, replace the expired certificate with the new certificate, described in Replacing Expired Certificates.
If a distribution provisioning profile expires, verify that you have a valid distribution certificate and renew the provisioning profile, described in Renewing Expired Provisioning Profiles.