新年回来,有比较足的闲暇时间,想起很久没写博客,今天多积累几篇,谢谢各位观看,记得给个赞哈。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 为每一个帐号提供了两个证书,这两个证书能够同时生效,这样在正在使用的证书过时以前,可使用另一个证书打包发布,让用户升级到新版本。