http://nixwang.com/2015/11/09/ios-dynamic-update/

前言

目前 iOS 上的动态更新方案主要有以下 4 种:

  • HTML 5
  • lua(wax)hotpatch
  • react native
  • framework

前面三种都是通过在应用内搭建一个运行环境来实现动态更新(HTML 5 是原生支持),在用户体验、与系统交互上有一定的限制,对开发者的要求也更高(至少得熟悉 lua 或者 js)。

使用 framework 的方式来更新可以不依赖第三方库,使用原生的 OC/Swift 来开发,体验更好,开发成本也更低。

由于 Apple 不希望开发者绕过 App Store 来更新 app,因此只有对于不需要上架的应用,才能以 framework 的方式实现 app 的更新。

主要思路

将 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 的根目录下。

加载

主要的代码如下:

  1. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
  1. - (UIViewController *)loadFrameworkNamed:(NSString *)bundleName {
    NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentDirectory = nil;
    if ([paths count] != 0) {
    documentDirectory = [paths objectAtIndex:0];
    }
  2.  
  3. NSFileManager *manager = [NSFileManager defaultManager];
    NSString *bundlePath = [documentDirectory stringByAppendingPathComponent:[bundleName stringByAppendingString:@".framework"]];
  4.  
  5. if (![manager fileExistsAtPath:bundlePath]) {
    NSLog(@"No framework update");
    bundlePath = [[NSBundle mainBundle]
    pathForResource:bundleName ofType:@"framework"];
  6.  
  7. 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;
    }
    }
  8.  
  9. 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;
    }
  10.  
  11. Class PublicAPIClass = NSClassFromString(@"PublicAPI");
    if (!PublicAPIClass) {
    NSLog(@"Unable to load class");
    return nil;
    }
  12.  
  13. NSObject *publicAPIObject = [PublicAPIClass new];
    return [publicAPIObject performSelector:@selector(mainViewController)];
    }

代码先尝试在 Document 目录下寻找更新后的 framework,如果没有找到,再在 main bundle 中寻找默认的 framework。
其中的关键是利用 OC 的动态特性 NSClassFromString 和 performSelector 加载 framework 的类并且执行其方法。

Framework 和 Host 工程资源共用

第三方库

  1. 1
  1. 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 版本动态加载时会提示找不到符号:

  1. 1
    2
    3
    4
    5
    6
    7
  1. 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 couldnt 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 couldnt be loaded.}

因为 Debug 版本暴露了所有自定义类的符号以便于调试,因此你的 framework 可以找到相应的符号,而 Release 版本则不会。

目前能想到的方法只有将相同的文件拷贝一份到 framework 工程里,并且更改类名。

访问 Framework 中的图片

在 storyboard/xib 中可以直接访问图片,代码中访问的方法如下:

  1. 1
  1. UIImage *image = [UIImage imageNamed:@"YourFramework.framework/imageName"]

注意:使用代码方式访问的图片不可以放在 xcassets 中,否则得到的将是 nil。并且文件名必须以 @2x/@3x 结尾,大小写敏感。因为 imageNamed: 默认在 main bundle 中查找图片。

常见错误

Architecture

  1. 1
    2
  1. dlopen(/path/to/framework, 9): no suitable image found. Did find:
    /path/to/framework: mach-o, but wrong architecture

这是说 framework 不支持当前机器的架构。通过

  1. 1
  1. 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 必须一致

如果不一致,否则会报下面的错误:

  1. 1
    2
  1. 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 的时候会报下面的错误:

  1. 1
    2
  1. [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 命令来验证。

  1. 1
  1. codesign -dv /path/to/YourApp.app

如果证书太旧,输出的结果如下:

  1. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  1. Executable=/path/to/YourApp.app/YourApp
    Identifier=com.company.yourapp
    Format=bundle with Mach-O thin (armv7)
    CodeDirectory v=20100 size=221748 flags=0x0(none) hashes=11079+5 location=embedded
    Signature size=4321
    Signed Time=20151021 上午10:18:37
    Info.plist entries=42
    TeamIdentifier=not set
    Sealed Resources version=2 rules=12 files=2451
    Internal 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 为每个账号提供了两个证书,这两个证书可以同时生效,这样在正在使用的证书过期之前,可以使用另外一个证书打包发布,让用户升级到新版本。

也就是说,可以使用另外一个证书来打包应用,并且可以覆盖安装使用旧证书打包的应用。详情可以看 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.

参考

http://devguo.com/blog/2015/06/16/iosdong-tai-geng-xin-frameworkshi-xian/

http://stackoverflow.com/questions/25909870/xcode-6-and-embedded-frameworks-only-supported-in-ios8

http://blog.csdn.net/like7xiaoben/article/details/44081257

https://www.apperian.com/mam-blog/impact-ios-8-app-wrapping/

http://stackoverflow.com/questions/9216485/how-to-manage-enterprise-distribution-certificate-expiration

iOS 利用 Framework 进行动态更新的更多相关文章

  1. iOS 静态库和动态库(库详解)

    什么是库 ? 库就是程序代码的集合,将N个文件组织起来,是共享程序代码的一种方式.库从本质上来说是一种可执行代码的二进制格式,可以被载入内存中执行. 库的分类 开源库:源代码是公开的,可以看到每个实现 ...

  2. iOS 静态库,动态库与 Framework 浅析

    静态库与动态库的区别 首先来看什么是库,库(Library)说白了就是一段编译好的二进制代码,加上头文件就可以供别人使用. 什么时候我们会用到库呢?一种情况是某些代码需要给别人使用,但是我们不希望别人 ...

  3. JSPatch – 动态更新iOS APP

    原文:http://blog.cnbang.net/works/2767/ JSPatch是最近业余做的项目,只需在项目中引入极小的引擎,就可以使用JavaScript调用任何Objective-C的 ...

  4. IOS App动态更新

    框架 JSPatch WaxPatch react-native   方案对比 目前已经有一些方案可以实现动态打补丁,例如WaxPatch,可以用Lua调用OC方法,相对于WaxPatch,JSPat ...

  5. iOS 静态库,动态库与 Framework

    iOS 静态库,动态库与 Framework     静态库与动态库的区别 首先来看什么是库,库(Library)说白了就是一段编译好的二进制代码,加上头文件就可以供别人使用. 什么时候我们会用到库呢 ...

  6. .NET基础篇——利用泛型与反射更新实体(ADO.NET Entity Framework)(转)

    自从ADO.NET Entity Framework面世以来,受到大家的热捧,它封装了大量代码生成的工具,用户只需要建立好实体之间的关系,系统就是会为用户自动成功了Add.Delete.CreateO ...

  7. iOS 静态库和动态库的区别&静态库的生成

    linux中静态库和动态库的区别 一.不同 库从本质上来说是一种可执行代码的二进制格式,可以被载入内存中执行.库分静态库和动态库两种. 1. 静态函数库 这类库的名字一般是libxxx.a:利用静态函 ...

  8. iOS 封装.framework 以及使用

    .framework是什么? .framework是什么? 这个问题相信做iOS的都知道答案. 在我们的日常开发中,经常会用到各种已经封装好的库,比如支付宝.微信SDK等等中的库,这些库可以给我们的开 ...

  9. JSPatch动态更新APP

    JSPatch,只需在项目中引入极小的引擎,就可以使用JavaScript调用任何Objective-C的原生接口,获得脚本语言的能力:动态更新APP,替换项目原生代码修复bug. 用途 是否有过这样 ...

随机推荐

  1. storm配置项目

    java.library.path: “/usr/local/lib:/opt/local/lib:/usr/lib”### storm.* configs are general configura ...

  2. <转>关闭 程序崩溃时 windows 正在检查该问题的解决方案

    本文转自:http://www.cnblogs.com/dabaopku/archive/2011/07/04/2097029.html 尤其是使用visual studio开发程序 ,自己特意thr ...

  3. 《BI那点儿事》数据流转换——排序

    排序转换允许对数据流中的数据按照某一列进行排序.这是五个常用的转换之一.连接数据源打开编辑界面,编辑这种任务.不想设置为排序列的字段不要选中,默认情况下所有列都会选中.如图所示,按照TotalSuga ...

  4. 学习mongo系列(七)aggregate() ,$group() 管道

    aggregate()聚合,主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果.有点类似sql语句中的 count(*) 接上边的数据库: > db.user.aggregate ...

  5. windows递归拷贝(或删除等操作)文件

    SHFileOperation 以拷贝为例. CString strFrom = ....._T("src");CString strTo = ....._T("dest ...

  6. C++ Primer 第九章 顺序容器

    由于书籍上写的已经很经典了,故大部分用图片的形式来阐述概念,代码纯手打进行验证. 1.顺序容器类型:vector.deque.list.forword_list.array.string. 2.顺序容 ...

  7. Mysqldump源码分析

    版权声明:本文由王珏原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/261 来源:腾云阁 https://www.qclou ...

  8. Android权限声明

    1.网络 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/ > < ...

  9. 20150514Linux下rpm包安装错误及解决方案

    (1)用rpm -ivh ***.rpm解压RedHat自带boost出现错误如下: warning: /media/RHEL_6.3 i386 Disc 1/Packages/boost-1.41. ...

  10. windows docker测试二 下载container

    安装dockertoolbox,提供了一个docker的界面工具 Kitematic 和字符终端: Docker Quickstart Terminal (这里安装的Kitematic 是Alpha版 ...