新年回來,有比較足的閒暇時間,想起很久沒寫博客,今天多積累幾篇,謝謝各位觀看,記得給個贊哈。html
說到目前 iOS 上的動態更新方案,主要有如下 4 種:react
HTML 5ios
lua(wax)hotpatchswift
react native服務器
framework架構
前面三種都是經過在應用內搭建一個運行環境來實現動態更新(HTML 5 是原生支持),在用戶體驗、與系統交互上有必定的限制,對開發者的要求也更高(至少得熟悉 lua 或者 js)。app
使用 framework 的方式來更新能夠不依賴第三方庫,使用原生的 OC/Swift 來開發,體驗更好,開發成本也更低。ide
因爲 Apple 不但願開發者繞過 App Store 來更新 app,所以**只有對於不須要上架的應用,才能以 framework 的方式實現 app 的更新。oop
主要實現思路:ui
將 app 中的某個模塊(好比一個 tab)的內容獨立成一個 framework 的形式動態加載,在 app 的 main bundle 中,當 app 啓動時從服務器上下載新版本的 framework 並加載便可達到動態更新的目的。
建立一個普通工程 DynamicUpdateDemo,其包含一個 framework 子工程 Module。也能夠將 Module 建立爲獨立的工程,建立工程的過程再也不贅述。
在主工程的 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 exists if (![manager fileExistsAtPath:bundlePath]) { NSLog(@"No framework update"); bundlePath = [[NSBundle mainBundle] pathForResource:bundleName ofType:@"framework"]; // Check if default bundle exists if (![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 = [PublicAPIClass new]; return [publicAPIObject performSelector:@selector(mainViewController)]; }
代碼先嚐試在 Document 目錄下尋找更新後的 framework,若是沒有找到,再在 main bundle 中尋找默認的 framework。
其中的關鍵是利用 OC 的動態特性 NSClassFromString
和 performSelector
加載 framework 的類而且執行其方法。
Class XXX is implemented in both XXX and XXX. One of the two will be used. Which one is undefined.
這是當 framework 工程和 host 工程連接了相同的第三方庫或者類形成的。
爲了讓打出的 framework 中不包含 host 工程中已包含的三方庫(如 cocoapods 工程編譯出的 .a 文件),能夠這樣:
刪除 Build Phases > Link Binary With Libraries
中的內容(若有)。此時編譯會提示三方庫中包含的符號找不到。
在 framework 的 Build Settings > Other Linker Flags
添加 -undefined dynamic_lookup
。**必須保證 host 工程編譯出的二進制文件中包含這些符號。**
嘗試過在 framework 中引用 host 工程中已有的文件,經過 Build Settings > Header Search Paths
中添加相應的目錄,Xcode 在編譯的時候能夠成功(由於添加了 -undefined dynamic_lookup
),而且 Debug 版本是能夠正常運行的,可是 Release 版本動態加載時會提示找不到符號:
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_$_BorderedView Referenced from: /var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework Expected in: flat namespace in /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_$_BorderedView Referenced from: /var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework Expected in: flat namespace in /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 版本暴露了全部自定義類的符號以便於調試,所以你的 framework 能夠找到相應的符號,而 Release 版本則不會。
目前能想到的方法只有將相同的文件拷貝一份到 framework 工程裏,而且更改類名。
在 storyboard/xib 中能夠直接訪問圖片,代碼中訪問的方法以下:
UIImage *image = [UIImage imageNamed:@"YourFramework.framework/imageName"]
注意:使用代碼方式訪問的圖片不能夠放在 xcassets 中,不然獲得的將是 nil。而且文件名必須以 @2x/@3x 結尾,大小寫敏感。由於 imageNamed:
默認在 main bundle 中查找圖片。
dlopen(/path/to/framework, 9): no suitable image found. Did find:/path/to/framework: mach-o, but wrong architecture
這是說 framework 不支持當前機器的架構。
經過
lipo -info /path/to/MyFramework.framework/MyFramework
能夠查看 framework 支持的 CPU 架構。
碰到這種錯誤,通常是由於編譯 framework 的時候,scheme 選擇的是模擬器,應該選擇**iOS Device**。
此外,若是沒有選擇**iOS Device**,編譯完成後,Products 目錄下的 .framework 文件名會一直是紅色,只有在 Derived Data 目錄下才能找到編譯生成的 .framework 文件。
系統在加載動態庫時,會檢查 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() error 1
此外,若是用來打包的證書是 iOS 8 發佈以前生成的,則打出的包驗證的時候會沒有 TeamIdentifier 這一項。這時在加載 framework 的時候會報下面的錯誤:
[deny-mmap] mapped file has no team identifier and is 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
若是證書太舊,輸出的結果以下:
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=not set Sealed Resources version=2 rules=12 files=2451Internal requirements count=1 size=188
注意其中的 TeamIdentifier=not set
。
採用 swift 加載 libswiftCore.dylib 這個動態庫的時候也會遇到這個問題,對此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 舊的證書會使全部用戶已經安裝的,用該證書打包的 app 沒法運行。
等等,咱們就跪在這裏了嗎?!
如今企業證書的有效期是三年,當證書過時時,其打包的應用就不能運行,那企業應用怎麼來更替證書呢?
Apple 爲每一個帳號提供了兩個證書,這兩個證書能夠同時生效,這樣在正在使用的證書過時以前,可使用另一個證書打包發佈,讓用戶升級到新版本。