实践一些想法。


一、前言

拜读了Casa前辈的《iOS应用架构谈》系列文章,收获颇丰,萌生了想实践的想法。2017年到现在,虽然写过不少项目,但是始终没有App作品在App Store上架过,也一直有这个小心愿。手机里面有个叫Splasher的App,是壁纸类型的App,虽然壁纸质量不错,但是App体验不太好,加载慢,界面冷酷无情。后来打听到它的API来自无版权可以商用的UnSplash。天时地利人和都有了,我的第一个期望上架App Store的作品:Splash,诞生了。

虽然只是壁纸类的没有多少功能的不需要多少道行就能写出来的App,但是还是想简单聊一聊。

二、思路

1,代码结构

与俺目前的习惯一致:方法结构按照life cycle、Delegate方法实现、event response、getters and setters顺序。

1
2
3
4
5
6
7
#pragma mark - Life cycle
#pragma mark - Event response
#pragma mark - Getter and setter
#pragma mark - Private methods
#pragma mark - Public methods
#pragma mark - UITableViewDelegate
#pragma mark - UITableViewDataSource

在viewDidload里面只做addSubview的事情,在viewDidLoad里面开一个layoutPageSubviews的方法,在这个里面创建Constraints并添加,在viewDidAppear里面做Notification的监听之类的事情。所有的属性都使用getter和setter,并且全部都放在最后。

我觉得作者说的还是有道理的,虽然某些getter一定会用到,再做一层懒加载貌似显得冗余,但是从结构上看更清晰。

2,公有特性

面向切片编程

  • 什么是切片?
    程序要完成一件事情,一定会有一些步骤,1,2,3,4这样。这里分解出来的每一个步骤我们可以认为是一个切片。
  • 什么是面向切片编程?
    你针对每一个切片的间隙,塞一些代码进去,在程序正常进行1,2,3,4步的间隙可以跑到你塞进去的代码,那么你写这些代码就是面向切片编程。
  • 为什么会出现面向切片编程?
    你要想做到在每一个步骤中间做你自己的事情,不用AOP也一样可以达到目的,直接往步骤之间塞代码就好了。但是事实情况往往很复杂,直接把代码塞进去,主要问题就在于:塞进去的代码很有可能是跟原业务无关的代码,在同一份代码文件里面掺杂多种业务,这会带来业务间耦合。为了降低这种耦合度,我们引入了AOP。
  • 如何实现AOP?
    Method Swizzling或者protocol的方式来实现拦截器(beforePerform、afterPerform)

使用AOP而非继承来实现公有特性。比如每个ViewController都有一个通用的navigationHeaderBar:

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
@implementation YAObjectIntercepter
+ (void)load {
[YAObjectIntercepter sharedInstance];
}

+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static YAObjectIntercepter *sharedInstance;
dispatch_once(&onceToken, ^{
sharedInstance = [YAObjectIntercepter new];
});
return sharedInstance;
}

- (instancetype)init {
if (self = [super init]) {
// 方法拦截
[UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionBefore usingBlock:^(id <AspectInfo> aspectInfo){
[self viewDidLoadWithViewController:[aspectInfo instance]];
} error:NULL];
}
return self;
}

#pragma mark - fake methods
- (void)viewDidLoadWithViewController:(UIViewController *)viewController {
if ([viewController isKindOfClass:UIViewController.class] && [NSStringFromClass(viewController.class) hasPrefix:@"YA"]) {
viewController.view.backgroundColor = UIColor.whiteColor;
viewController.navigationHeaderBar.backgroundColor = UIColor.whiteColor;
[viewController.view addSubview:viewController.navigationHeaderBar];
viewController.navigationHeaderBar.frame = CGRectMake(0, kIPHONEX_TOP, kScreenWidth, kNavigationBarHeight);
[viewController.navigationHeaderBar.leftButton setBackgroundImage:[UIImage imageNamed:@"feed_back"] forState:UIControlStateNormal];
[viewController.navigationHeaderBar.leftButton setBackgroundImage:[UIImage imageNamed:@"feed_back"] forState:UIControlStateHighlighted];
[viewController.navigationHeaderBar.leftButton addTarget:viewController action:@selector(popViewControllerWithAnimation) forControlEvents:UIControlEventTouchUpInside];
}
}
@end

3,网络层设计

1,网络层数据通信以Delegate为主,Notification为辅(网络信号从2G变成3G变成4G变成Wi-Fi)。
2,提供reformer机制来处理网络层反馈的数据,交付NSDictionary给业务层,使用Const字符串作为Key来保持可读性。
3,网络层上部分使用离散型设计,下部分使用集约型设计。设计合理的继承机制,让派生出来的APIManager受到限制,避免混乱。
关于集约型的API调用和离散型的API调用,我倾向于这样:对外提供一个BaseAPIManager来给业务方做派生,在BaseManager里面采用集约化的手段组装请求,放飞请求,然而业务方调用API的时候,则是以离散的API调用方式来调用。如果你的App只提供了集约化的方式,而没有离散方式的通道,那么我建议你再封装一层,便于业务方使用离散的API调用方式来放飞请求。

大型App可以使用封装AFN的YTKNetwork,而小型App由于API少,参数不复杂,很多就直接使用一个类,暴露Get和Post接口就可以了。

下层:统一接口,以离散型封装AFNetworking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef NSString *YAServerInterfaceName;

// 首页Feed接口
extern YAServerInterfaceName const kYAServerInterfaceFeed;
// 搜索接口
extern YAServerInterfaceName const kYAServerInterfaceSearch;

@interface YANetworkManager : NSObject
+ (instancetype)manager;

- (NSURLSessionDataTask *)requestWithInterfaceName:(YAServerInterfaceName)interfaceName
parameters:(NSDictionary *)parameters
progress:(void (^)(NSProgress *))downloadProgress
success:(void (^)(NSURLSessionDataTask *, id))success
failure:(void (^)(NSURLSessionDataTask *, NSError *))failure;
@end

把接口以及公共参数进行封装,避免散落一地。使用时只需要传入InterfaceName即可。每个业务都可以有自己的manager,成功或者失败回调以block的形式回传,供各个业务自己发挥。

上层:统一APIManager,以集约型的delegate进行回调

1
2
3
4
5
6
/// API manager的基类
@interface YABaseAPIManager : NSObject
@property (nonatomic, assign) BOOL noMoreData;
- (void)fetchNetworkDataWithParameters:(NSDictionary *)parameters;
- (void)cancelPreviousRequest;
@end

BaseAPIManager负责派生各个业务的APIManager,提供数据请求及取消数据请求接口。BaseAPIManager还约定了子类需要实现的方法:成功回调、失败回调、以及接口名称。具体请求的参数怎么配置、成功回调怎么处理、失败回调怎么处理都由子类决定。

1
2
3
4
5
6
@protocol YABaseAPIManagerChildProtocol <NSObject>
@required
- (YAServerInterfaceName)interfaceName;
- (void)APIManager:(YABaseAPIManager *)manager finishedWithFailedError:(NSError *)error;
- (void)APIManager:(YABaseAPIManager *)manager finishedWithSuccessResult:(id)result;
@end

4,从MVC、MVVM到去Model化

MVC
M应该做的事:
给ViewController提供数据;
给ViewController存储数据提供接口;
提供经过抽象的业务基本组件,供Controller调度;

C应该做的事:
管理View Container的生命周期;
负责生成所有的View实例,并放入View Container;
监听来自View与业务有关的事件,通过与Model的合作,来完成对应事件的业务;

V应该做的事:
响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等;
界面元素表达

MVVM
在MVC的基础上,把C拆出一个ViewModel专门负责数据处理的事情,就是MVVM。然后,为了让View和ViewModel之间能够有比较松散的绑定关系,于是我们使用ReactiveCocoa,因为苹果本身并没有提供一个比较适合这种情况的绑定方法。iOS领域里KVO,Notification,block,delegate和target-action都可以用来做数据通信,从而来实现绑定,但都不如ReactiveCocoa提供的RACSignal来的优雅,如果不用ReactiveCocoa,绑定关系可能就做不到那么松散那么好,但并不影响它还是MVVM。

去Model化
保留原始数据,定义reform协议,遵守该协议的对象来处理manager的原始数据,处理后的数据依然是字典或者数组。

1
2
3
@protocol YAFeedReformerProtocol <NSObject>
- (NSArray *)reformData:(id)data manager:(YAFeedAPIManager *)manager;
@end

每类数据对应一个Reformer:

1
2
3
4
5
6
7
extern NSString * const kPropertyListDataKeyFeedID;             ///< id
extern NSString * const kPropertyListDataKeyFeedThumbUrl; ///< 拇指图url
extern NSString * const kPropertyListDataKeyFeedSmallUrl; ///< 小图url
extern NSString * const kPropertyListDataKeyFeedRegularUrl; ///< 常规图url

@interface YAFeedReformer : NSObject <YAFeedReformerProtocol>
@end

根据字典的key可以直接使用:

1
2
3
4
5
6
7
- (void)setPhotoInfo:(NSDictionary *)photoInfo {
NSURL *url = nil;
if (YASettingManager.columnCount <= 2) {
url = [NSURL URLWithString:photoInfo[kPropertyListDataKeyFeedRegularUrl]];
}
// ...
}

5,网络优化

针对链接建立环节的优化

  • 使用缓存手段减少请求的发起次数(API名字和参数拼成一个字符串然后取MD5作为key,存储对应返回的数据)
  • 使用策略来减少请求的发起次数(下拉刷新、条件筛选取消原先存在的请求,用户日志满足一定数量再上传)

针对DNS域名解析做的优化
原因:API请求在DNS解析阶段的耗时会很多(网络信号源会经常变换、自己的App所做的DNS缓存会被别的DNS缓存给挤出去被清理掉、墙 这三个原因造成链路的DNS缓存很快失效相当于没有,于是直接走IP请求,绕过DNS服务的耗时)
方案:本地有一份IP列表,这些IP是所有提供API的服务器的IP,每次应用启动的时候,针对这个列表里的所有IP取ping延时时间,然后取延时时间最小的那个IP作为今后发起请求的IP地址。一般都是在应用启动的时候获得本地列表中所有IP的ping值,然后通过NSURLProtocol的手段将URL中的HOST修改为我们找到的最快的IP。另外,这个本地IP列表也会需要通过一个API来维护,一般是每天第一次启动的时候读一次API,然后更新到本地。

针对链接传输数据量的优化
压缩。

针对链接复用的优化
HTTP/2.0。

判断API的调用请求是来自于经过授权的APP?
设计签名
服务端需要给你一个密钥,每次调用API时,你使用这个密钥再加上API名字和API请求参数算一个hash出来,然后请求的时候带上这个hash。服务端收到请求之后,按照同样的密钥同样的算法也算一个hash出来,然后跟请求带来的hash做一个比较,如果一致,那么就表示这个API的调用者确实是你的APP。为了不让别人也获取到这个密钥,你最好不要把这个密钥存储在本地,直接写死在代码里面就好了。另外适当增加一下求Hash的算法的复杂度,那就是各种Hash算法(比如MD5)加点盐,再回炉跑一次Hash啥的。这样就能解决第一个目的了:确保你的API是来自于你自己的App。

网络优化在Splash上的实践主要是ip直连。Casa前辈给出了NSEtcHosts,但是它处理NSURLConnection,过时了,我学习借鉴写了个NSURLSession版本的。当然,思路都是一样的,自定义NSURLProtocol。增加了一点点功能:

  • 支持HTTPS
  • 直连ip失败后自动切回host
  • 支持AFN
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
@protocol YAHostsConfiguration
// 配置host与IP的映射
- (void)resolveHostName:(NSString *)hostName
mapIPAddress:(NSString *)IPAddress;
@end

@interface YADomainURLProtocol : NSURLProtocol
+ (void)configureHostsWithBlock:(void (^)(id <YAHostsConfiguration> configuration))block;
@end



@interface YAHostsConfiguration : NSObject <YAHostsConfiguration>
@property (nonatomic, strong) NSMutableDictionary *mutableIPAddressesByHostName;
- (NSString *)IPAddressForHostName:(NSString *)hostName;
@end
@implementation YAHostsConfiguration

- (instancetype)init {
if (self = [super init]) {
_mutableIPAddressesByHostName = [NSMutableDictionary dictionary];
}
return self;
}

- (NSString *)IPAddressForHostName:(NSString *)hostName {
return self.mutableIPAddressesByHostName[hostName.lowercaseString];
}

- (void)resolveHostName:(NSString *)hostName mapIPAddress:(NSString *)IPAddress {
self.mutableIPAddressesByHostName[hostName.lowercaseString] = IPAddress;
}
@end



static NSString * const kURLProtocolHostModifiedKey = @"kURLProtocolHostModifiedKey";

@interface YADomainURLProtocol () <NSURLSessionTaskDelegate>
@property (atomic, strong, readwrite) NSURLSession *session;
@end

@implementation YADomainURLProtocol
+ (YAHostsConfiguration *)sharedConfiguration {
static YAHostsConfiguration * sharedConfiguration = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedConfiguration = [YAHostsConfiguration new];
});

return sharedConfiguration;
}

+ (void)configureHostsWithBlock:(void (^)(id <YAHostsConfiguration> configuration))block {
if (block) block([self sharedConfiguration]);
}

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
BOOL isHttps = [request.URL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame;
if (isHttps) {
// 没有处理过且有映射
if (![self propertyForKey:kURLProtocolHostModifiedKey inRequest:request] && [[self sharedConfiguration] IPAddressForHostName:request.URL.host]) {
YALog(@"命中ip直连");
return YES;
}
return NO;
}
return NO;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
// 容错
if (request.URL.host.length == 0) return request;

NSMutableURLRequest *mutableRequest = [request mutableCopy];
NSURLComponents *URLComponents = [NSURLComponents componentsWithString:mutableRequest.URL.absoluteString];
URLComponents.scheme = @"https";
URLComponents.host = [[self sharedConfiguration] IPAddressForHostName:URLComponents.host];
mutableRequest.URL = [URLComponents URL];
[self setProperty:@(YES) forKey:kURLProtocolHostModifiedKey inRequest:mutableRequest];
// 设置原先的host
[mutableRequest setValue:request.URL.host forHTTPHeaderField:@"host"];
return mutableRequest;
}

- (void)startLoading {
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue new]];
[[self.session dataTaskWithRequest:self.request] resume];
}

- (void)stopLoading {
[self.session finishTasksAndInvalidate];
self.session = nil;
}

#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
// 加载完成: error回调
[self.client URLProtocol:self didFailWithError:error];
// 出错: 取消ip直连
NSString *host = [self.request valueForHTTPHeaderField:@"host"];
[YADomainURLProtocol sharedConfiguration].mutableIPAddressesByHostName[host.lowercaseString] = nil;
} else {
// 加载完成: success回调
[self.client URLProtocolDidFinishLoading:self];
}
}

- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
// 接收response回调
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
// 接收数据回调
[self.client URLProtocol:self didLoadData:data];
}

#pragma mark - 证书
// https: 证书
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
if (!challenge) return;

NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *credential = nil;
NSString *host = [self.request.allHTTPHeaderFields objectForKey:@"host"];
if (!host) host = self.request.URL.host;

if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust] && [self createServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
// 处理服务器自制证书,参照AFN
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
completionHandler(disposition, credential);
}

// 验证SSL握手过程中服务端返回的证书是否可信任
// 参考AFN中 AFSecurityPolicy 模块的代码
// 注:只适用于一台服务器的IP只配置了一个默认的域名和SSL证书的情况
- (BOOL)createServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain {
// 创建证书校验策略
NSMutableArray *policies = [NSMutableArray array];
// 需要验证请求的域名与证书中声明的CN字段是否一致
if (domain) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
// 绑定校验策略到服务端返回的证书上
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
SecTrustResultType result;
CFErrorRef error;
SecTrustEvaluate(serverTrust, &result);
// 评估当前serverTrust是否可信任,
// 当 result 为 kSecTrustResultUnspecified 或 kSecTrustResultProceed 的情况下,serverTrust 可以被验证通过
return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}
@end

只需要这么使用:

1
2
3
4
[NSURLProtocol registerClass:YADomainURLProtocol.class];
[YADomainURLProtocol configureHostsWithBlock:^(id<YAHostsConfiguration> configuration) {
[configuration resolveHostName:@"www.baidu.com" mapIPAddress:@"220.181.38.150"];
}];

对于AFN,配置configuration即可

1
2
3
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.protocolClasses = @[YADomainURLProtocol.class];
AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration];

注意:目前没有考虑单IP多域名证书(SNI)。

对于这种场景,iOS上层网络 API NSURLConnection/NSURLSession 都没有提供相关方法进行 SNI 字段的配置,因此需要 Socket 层级的底层网络库,例如 CFNetwork,来实现 IP 直连网络请求适配方案。
可以参考:阿里云的《iOS HTTPS SNI 业务场景“IP直连”方案说明

6,持久化

持久化方案有哪些?

  • NSUserDefault: 小规模数据,弱业务相关数据,都可以放到NSUserDefault里面,内容比较多的数据,强业务相关的数据就不太适合NSUserDefault了。
  • Keychain : Keychain是苹果提供的带有可逆加密的存储机制,普遍用在各种存密码的需求上。另外,由于App卸载只要系统不重装,Keychain中的数据依旧能够得到保留,以及可被iCloud同步的特性,大家都会在这里存储用户唯一标识串。所以有需要加密、需要存iCloud的敏感小数据,一般都会放在Keychain。
  • 文件存储:文件存储包括了Plist、archive、Stream等方式,一般结构化的数据或者需要方便查询的数据,都会以Plist的方式去持久化。Archive方式适合存储平时不太经常使用但很大量的数据,或者读取之后希望直接对象化的数据,因为Archive会将对象及其对象关系序列化,以至于读取数据的时候需要Decode很花时间,Decode的过程可以是解压,也可以是对象化,这个可以根据具体中的实现来决定。Stream就是一般的文件存储了,一般用来存存图片啊啥的,适用于比较经常使用,然而数据量又不算非常大的那种。
  • 数据库存储:苹果自带了一个Core Data,其他还有FMDB。数据库方案主要是为了便于增删改查,当数据有状态和类别的时候最好还是采用数据库方案比较好。因为你不可能通过文件系统遍历文件去甄别你需要获取的属于某个状态或类别的数据,这么做成本就太大了。当然,特别大量的数据也不适合直接存储数据库,比如图片或者文章这样的数据,一般来说,都是数据库存一个文件名,然后这个文件名指向的是某个图片或者文章的文件。如果真的要做全文索引这种需求,建议最好还是挂个API丢到服务端去做。

上面是前辈总结的面试题答案😂

接下来是持久化的实践:
与以往不同的是,之前采用MVC或者MVVM模式,存在数据库的是Model,而现在去Model化了,存原始数据呢,还是存Reform之后的数据。我选择存储原始数据,原因有二:
1,Reformer有多个,但是原始数据只有一份,只存储一份原始数据避免冗余。
2,如果存储原始数据,那从数据库取出的数据和从网络获取的数据,格式将会是一样的,只需要一种处理方式。

怎么存储?每条photo信息中的photo_id作为主键,再加上时间戳用于筛选,photo_content自然就是存储的原始信息了:

1
2
3
4
5
6
CREATE TABLE IF NOT EXISTS "t_photoList" (
"photo_id" text,
"photo_content" text,
"photo_date" INTEGER,
PRIMARY KEY("photo_id")
);

这样职责更清晰了:APIManager负责从网络或者数据库取数据以及持久化数据,Reformer只负责格式化APIManager的数据。对于控制器来说,它做的也很简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)feedAPIDidSuccessWithManager:(YAFeedAPIManager *)manager
isNewData:(BOOL)isNewData {
NSArray <NSDictionary *> *array = [manager fetchDataWithReformer:self.feedReformer];
if (isNewData) {
[self.photoArray removeAllObjects];
[self.refreshHeader endRefreshing];
}
[self.photoArray addObjectsFromArray:array];
if (manager.noMoreData) {
// 没有更多数据
[self.refreshFooter endRefreshingWithNoMoreData];
} else {
[self.refreshFooter endRefreshing];
}
[self.feedCollectionView reloadData];
}

7,动态部署方案

(1)Web App
其实所谓的web app,就是通过手机上的浏览器进行访问的H5页面。这个H5页面是针对移动场景特别优化的,比如UI交互等。

  • 优点
    无需走苹果流程,所有苹果流程带来的成本都能避免,包括审核周期、证书成本等。
    版本更新跟网页一样,随时生效。
    不需要Native App工程师的参与,而且市面上已经有很多针对这种场景的框架。
  • 缺点
    重度依赖网络环境、流畅度不如Native、很难做好本地持久化(只能提供账户体系,对应账户的持久化数据全部存在服务端)、即时响应方案、远程通知实现方案、移动端传感器的使用方案复杂,维护难度大。
    安全问题,H5页面等于是所有东西都暴露给了用户,如果对安全要求比较高的,很多额外的安全机制都需要在服务端实现。
  • 总结
    web app一般是创业初期会重点考虑的方案,因为迭代非常快,而且创业初期的主要目标是需要验证模式的正确性,并不在于提供非常好的用户体验,只需要完成闭环即可。

(2)Hybrid App
通过市面上各种Hybrid框架,来做H5和Native的混合应用,或者通过JS Bridge来做到H5和Native之间的数据互通。

  • 优点
    除了要承担苹果流程导致的成本以外,具备所有web app的优势、能够访问本地数据、设备传感器等
  • 缺点
    跟web app一样存在过度依赖网络环境的问题、用户体验也很难做到很好、安全性问题依旧存在、大规模的数据交互很难实现,例如图片在本地处理后,将图片传递给H5
  • 总结
    Hybrid方案更加适合跟本地资源交互不是很多,然后主要以内容展示为主的App。在天猫App中,大量地采用了JS Bridge的方式来让H5跟Native做交互,因为天猫App是一个以内容展示为主的App,且营销活动多,周期短,比较适合Hybrid。

(3)React-Native

  • 优点
    响应速度很快,只比Native慢一点,比webview快很多。
    能够做到一定程度上的动态部署
  • 缺点
    组装页面的元素需要Native提供支持,一定程度上限制了动态部署的灵活性。
  • 总结
    由于View的展示和View的事件响应分属于不同的端,展示部分的描述在JS端,响应事件的监听和描述都在Native端,通过Native转发给JS端。所以,从做动态部署的角度上讲,React-Native只能动态部署新View,不能动态部署新View对应的事件。
    View的原型需要从Native中取,以后某个页面需要添加某个复杂的view的时候,需要从现有的组件中拼装。
    它解决的是如何不使用Objc/Swift来写iOS App的View的问题,对于如何通过不发版来给已发版的App更新功能这样的问题,帮助有限。

(4)Lua Patch
waxPatch的主要原理是通过lua来针对objc的方法进行替换,由于lua本身是解释型语言,可以通过动态下载得到,因此具备了一定的动态部署能力。然而iOS系统原生并不提供lua的解释库,所以需要在打包时把lua的解释库编译进app。

  • 优点
    能够通过下载脚本替换方法的方式,修改本地App的行为。
    执行效率较高
  • 缺点
    对于替换功能来说,lua是很不错的选择。但如果要添加新内容,实际操作会很复杂,很容易改错,小问题变成大问题
  • 总结
    lua的解决方案在一定程度上解决了动态部署的问题。实际操作时,一般不使用它来做新功能的动态部署,主要还是用于修复bug时代码的动态部署。

(5)Javascript Patch
这个工作原理其实跟上面说的lua那套方案的工作原理一样,只不过是用javascript实现。

  • 优点
    打包时不用将解释器也编译进去,iOS自带JavaScript的解释器。
  • 缺点
    同Lua方案的缺点
  • 总结
    在对app打补丁的方案中,目前我更倾向于使用JSPatch的方案,在能够完成Lua做到的所有事情的同时,还不用编一个JS解释器进去,而且会javascript的人比会lua的人多,技术储备比较好做。

(6)JSON Descripted View
使用JSON来描述一个View应该有哪些元素,以及元素的位置,以及相关的属性,比如背景色,圆角等等。然后本地有一个解释器来把JSON描述的View生成出来。
这跟React-Native有点儿像,一个是JS转Native,一个是JSON转Native。但是同样有的问题就是事件处理的问题,在事件处理上,React-Native做得相对更好。因为JSON不能够描述事件逻辑,所以JSON生成的View所需要的事件处理都必须要本地事先挂好。

  • 优点
    能够自由生成View并动态部署
  • 缺点
    天猫实际使用下来,发现还是存在一定的性能问题,不够快
    事件需要本地事先写好,无法动态部署事件
  • 总结
    其实JSON描述的View比React-Native的View有个好处就在于对于这个View而言,不需要本地也有一套对应的View,它可以依据JSON的描述来自己生成。然而对于事件的处理是它的硬伤,所以JSON描述View的方案,一般比较适用于换肤,或者固定事件不同样式的View,比如贴纸。

作者总结地实在是太全面太好了(崇拜脸😁)。
目前来看,使用比较广泛的是JS Patch。手百参考了一下它的原理,自己又写了一个,用于热修复,之前老板让调研过,最近用了几次。

在Splash中目前还没有实践热更新方案。

三、总结

1.0.0版本已经发布了,但是最近开发人员注册好像出了点问题,付款老是失败,然后就没上线。

github放这了:https://github.com/chenYalun/Splash