目前,流行的热更新框架绝大多数都被Apple审核团队给禁止使用。为了能更好的实现app的热更新功能,现基于iOS纯原生代码书写了一套热更新功能。
这种热更新的思路适用于所有的iOS app开发。上面只是我举的一个例子,具体大家可以根据我这个思路举一反三,不同的项目实现的方式上都是大同小异。
注:请大家不要用这种热更新方法去上架一些违规的app,我之所以把这种思路和源码贡献出来是当一个线上的项目出现某种紧急问题时,希望大家可以采用这种方式尽快对其进行修复,从而将损失降到最低
下面我是按照基于Cordova的混合式开发项目实现的app热更新功能
具体的实现思路如下:
源码干货,请看下面:(注:下面的代码是基于Cordova框架用原生代码实现热更新功能的)。
#import <Foundation/Foundation.h>
#import "SSZipArchive.h"
#import "SYLineProgressView.h"
#import "AFNetworking.h"
NS_ASSUME_NONNULL_BEGIN
@interface DownloadTool : NSObject <SSZipArchiveDelegate>
@property (nonatomic, strong)NSUserDefaults *Defaults;
@property(nonatomic,copy)NSString *urlStr;
@property (nonatomic, strong) UIButton *confirmBtn;
@property (nonatomic, strong) SYLineProgressView *lineProgress;
@property (nonatomic, strong) UILabel *promptTitle;
@property (nonatomic, strong) UILabel *promptContent;
@property (nonatomic, copy) NSString *stateStr;
@property (nonatomic, copy) NSString *fileName;
@property (nonatomic, copy) NSString *downloadedFilePath;
@property (nonatomic, copy) NSString *NVersionH5;
/**
创建单例对象
@return 单例对象
*/
+ (instancetype)sharedInstance;
//下载地址
-(void)loadUrl:(NSString *)url;
/**
初始化
*/
@end
NS_ASSUME_NONNULL_END
#import "DownloadTool.h"
@implementation DownloadTool
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static DownloadTool *manager;
dispatch_once(&onceToken, ^{
manager = [[DownloadTool alloc] init];
});
return manager;
}
-(instancetype)init{
self = [super init];
if (self) {
// [self networkReachability];
}
return self;
}
//-(void)setUI{
// self.lineProgress = [[SYLineProgressView alloc] initWithFrame:CGRectMake(20.0, ScreenHeight/2 - 100, (self.view.frame.size.width - 40.0), 20)];
// [self.view addSubview:self.lineProgress];
// self.lineProgress.layer.cornerRadius = 10;
// self.lineProgress.lineWidth = 1.0;
// self.lineProgress.lineColor = [UIColor redColor];
// self.lineProgress.progressColor = [UIColor redColor];
// self.lineProgress.defaultColor = [UIColor yellowColor];
// self.lineProgress.label.textColor = [UIColor greenColor];
// self.lineProgress.label.hidden = NO;
// [self.lineProgress initializeProgress];
// self.promptTitle = [[UILabel alloc]initWithFrame:CGRectMake(20, ScreenHeight/2 - 150, ScreenWidth - 40, 30)];
// self.promptTitle.text = @"";
// self.promptTitle.font = [UIFont systemFontOfSize:18];
// self.promptTitle.textAlignment = NSTextAlignmentCenter;
// [self.view addSubview:self.promptTitle];
// self.promptContent = [[UILabel alloc]initWithFrame:CGRectMake(20, ScreenHeight/2-60, ScreenWidth-40, 30)];
// self.promptContent.numberOfLines = 0;
// self.promptContent.font = [UIFont systemFontOfSize:14];
// self.promptContent.textAlignment = NSTextAlignmentCenter;
// self.promptContent.textColor = [UIColor redColor];
// self.promptContent.text = @"";
// [self.view addSubview:self.promptContent];
// self.confirmBtn = [UIButton buttonWithType:UIButtonTypeCustom];
// self.confirmBtn.frame = CGRectMake(ScreenWidth/2 - 50, CGRectGetMaxY(self.promptContent.frame)+20, 100, 50);
// self.confirmBtn.layer.cornerRadius = 10;
// self.confirmBtn.layer.masksToBounds = YES;
// self.confirmBtn.hidden = YES;
// self.confirmBtn.backgroundColor = [UIColor blueColor];
// [self.confirmBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
// [self.confirmBtn setTitle:@"确定" forState:UIControlStateNormal];
// self.confirmBtn.titleLabel.font = [UIFont systemFontOfSize:16];
// [self.confirmBtn addTarget:self action:@selector(confirmBtnClik:) forControlEvents:UIControlEventTouchUpInside];
// [self.view addSubview:self.confirmBtn];
// self.stateStr = @"";
//}
-(void)confirmBtnClik:(UIButton *)stateBtn{
// dispatch_async(dispatch_get_main_queue(), ^{
//进行UI操作
// [self dismissViewControllerAnimated:YES completion:^{
if ([self.stateStr isEqualToString:@"YES"]) {
[self delegateViewControllerDidClickwithString:@"1"];
// [self.delegate delegateViewControllerDidClickwithString:@"1"];
}else{
[self delegateViewControllerDidClickwithString:@"0"];
// [self.delegate delegateViewControllerDidClickwithString:@"0"];
}
// }];
//});
}
-(void)loadUrl:(NSString *)url{
_Defaults = [NSUserDefaults standardUserDefaults];
self.stateStr = @"";
// NSURL *url = [NSURL URLWithString:@"http://114.55.179.182:9009/mock/13/api/v1/demo/hot-fix"];
self.urlStr = url;
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:url] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
return;
NSLog(@"数据请求失败:%@",error.localizedDescription);
}else{
NSLog(@"数据请求成功");
NSError *rror;
NSDictionary * dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&rror];
if (rror != nil) {
NSLog(@"数据解析失败的原因:%@",rror.localizedDescription);
return;
}
NSLog(@"%@",dict);
self.NVersionH5 = [NSString stringWithFormat:@"%@",dict[@"h5version"]];
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
NSString *app_Version = [infoDictionary objectForKey:@"CFBundleShortVersionString"];
if ([self compareLastVersion:[NSString stringWithFormat:@"%@",dict[@"iosversion"]]]>[self compareLastVersion:app_Version]) {
return;
}else{
if ([self compareLastVersion:[NSString stringWithFormat:@"%@",dict[@"h5version"]]]>[self compareLastVersion:[NSString stringWithFormat:@"%@",[_Defaults objectForKey:@"iosH5Version"]]]) {
[self downloadWWWzip:[NSString stringWithFormat:@"%@",dict[@"iosH5"]]];
}else{
return;
}
}
}
}
];
//4.执行任务
[dataTask resume];
}
-(int)compareLastVersion:(NSString *)VersionStr{
return [[VersionStr stringByReplacingOccurrencesOfString:@"." withString:@""] intValue];
}
- (void)delegateViewControllerDidClickwithString:(NSString *)string{
NSLog(@"传值成功");
// [_Defaults setObject:string forKey:@"updateStatus"];
if ([string isEqualToString:@"1"]) {
[_Defaults setObject:self.NVersionH5 forKey:@"NewiosH5Version"];
[_Defaults setObject:@"NO" forKey:@"state"];
}
}
-(void)downloadWWWzip:(NSString *)pathUrl{
// self.promptTitle.text = @"文件正在更新下载中...";
//远程地址
NSURL *URL = [NSURL URLWithString:pathUrl];
//默认配置
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
//请求
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
// });
NSURLSessionDownloadTask * downloadTask =[manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
// double curr=(double)downloadProgress.completedUnitCount;
// double total=(double)downloadProgress.totalUnitCount;
// NSLog(@"下载进度==%.2f",curr/total);
// CGFloat progress = curr/total;
// dispatch_async(dispatch_get_main_queue(), ^{
//进行UI操作 设置进度条
// self.lineProgress.progress = progress;
// self.progressView.contentLabel.text = [NSString stringWithFormat:@"%.2f%%",progress*100];
// });
} destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
//- block的返回值, 要求返回一个URL, 返回的这个URL就是文件的位置的路径
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
//再这之前先删除本地文件夹里面相同的文件夹
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *contents = [fileManager contentsOfDirectoryAtPath:cachesPath error:NULL];
NSEnumerator *e = [contents objectEnumerator];
NSString *filename;
NSString *extension = @"zip";
while ((filename = [e nextObject])) {
if ([[filename pathExtension] isEqualToString:extension]) {
NSError *error;
[fileManager removeItemAtPath:[cachesPath stringByAppendingPathComponent:filename] error:&error];
if (!error) {
// NSLog(@"删除本地zip文件成功!");
// self.promptTitle.text = @"删除本地zip文件成功!";
}else{
// NSLog(@"删除本地zip文件失败!%@",error.localizedDescription);
// self.promptTitle.text = @"删除本地zip文件失败!";
}
}
}
NSString *path = [cachesPath stringByAppendingPathComponent:response.suggestedFilename];
return [NSURL fileURLWithPath:path];
} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
//设置下载完成操作
if (!error) {
// dispatch_async(dispatch_get_main_queue(), ^{
//进行UI操作 设置进度条
// self.promptTitle.text = @"文件下载成功!";
// });
NSLog(@"------- 下载成功-------");
// filePath就是你下载文件的位置,你可以解压,也可以直接拿来使用
_downloadedFilePath = [filePath path];// 将NSURL转成NSString
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
//再这之前先删除本地文件夹里面相同的文件夹
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *contents = [fileManager contentsOfDirectoryAtPath:cachesPath error:NULL];
NSEnumerator *e = [contents objectEnumerator];
NSString *extension = @"zip";
while ((_fileName = [e nextObject])) {
if ([[_fileName pathExtension] isEqualToString:extension]) {
// [self changeFolderName:@"www.zip" beforeName:_fileName];
// _downloadedFilePath = afterFolder;
NSArray *documentArray = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *path = [[documentArray lastObject] stringByAppendingPathComponent:@"Preferences"];
[self releaseZipFilesWithUnzipFileAtPath:_downloadedFilePath Destination:path];
}
}
}else{
// NSLog(@"下载失败");
}
}];
[downloadTask resume];
}
#pragma mark 解压
- (void)releaseZipFilesWithUnzipFileAtPath:(NSString *)zipPath Destination:(NSString *)unzipPath{
NSLog(@"zipPath = %@,unzipPath = %@",zipPath,unzipPath);
[SSZipArchive unzipFileAtPath:zipPath toDestination:unzipPath overwrite:YES password:nil progressHandler:^(NSString * _Nonnull entry, unz_file_info zipInfo, long entryNumber, long total) {
// entry : 解压出来的文件名
//entryNumber : 第几个, 从1开始
//total : 总共几个
NSLog(@"progressHandler:%@, entryNumber:%ld, total:%ld names:%@", entry, entryNumber, total,unzipPath);
// NSLog(@"解压进度==%.2ld",entryNumber/total);
// CGFloat progress = entryNumber/total;
// dispatch_async(dispatch_get_main_queue(), ^{
// self.promptTitle.text = @"文件正在解压中...";
//进行UI操作 设置进度条
// self.lineProgress.progress = progress;
// self.lineProgress.hidden = YES;
// });
} completionHandler:^(NSString * _Nonnull path, BOOL succeeded, NSError * _Nullable error) {
//path : 被解压的压缩吧全路径
//succeeded 是否成功
// error 错误信息
NSLog(@"completionHandler:%@, , succeeded:%d, error:%@,unzipPath=%@", path, succeeded, error,unzipPath);
if (succeeded) {
// dispatch_async(dispatch_get_main_queue(), ^{
// self.promptTitle.text = @"文件解压成功!";
// });
//移除解压完的zip文件
NSFileManager * fileManager = [NSFileManager defaultManager];
if([fileManager fileExistsAtPath:zipPath]){
if(![fileManager removeItemAtPath:zipPath error:nil]){
// NSLog(@"zip删除失败");
}else{
// NSLog(@"zip删除成功");
}
}
// BOOL ok = [fileManager fileExistsAtPath:[NSString stringWithFormat:@"%@/www/index.html",unzipPath]];
// if (ok == NO) {
// return;
// }
NSString *indexPath = [NSString stringWithFormat:@"%@/www/index.html",unzipPath];
// 读取文件
NSData *txtCon = [NSData dataWithContentsOfFile:indexPath];
NSString *dataStr = [[NSString alloc]initWithData:txtCon encoding:NSUTF8StringEncoding];
if (dataStr == nil) {
return;
}
// 写入文件内容
NSString *changeStr = [self withOriginalString:dataStr subStringFrom:@"window.start;" to:@"window.end;"];
// NSString *H5VersionStr = [[NSString stringWithFormat:@"%@",testArray[7]] stringByReplacingOccurrencesOfString:@"\n" withString:@""];
// NSString * H5VersionStr= [NSString stringWithFormat:@"window._version_ = '%@'",[_Defaults objectForKey:@"iosH5Version"]];
NSString *stringWindow = [NSString stringWithFormat:@"%@",[_Defaults objectForKey:@"indexText"]];
NSString *strUrl = [dataStr stringByReplacingOccurrencesOfString:changeStr withString:stringWindow];
NSString *changeStr0 = [self withOriginalString:strUrl subStringFrom:@"window.start;" to:@"window.end;"];
static NSString *H5VersionStr;
NSArray *testArray = [changeStr0 componentsSeparatedByString:@";"];
if (testArray.count>0) {
for (int i = 0; i < testArray.count; i++) {
NSString *string = [NSString stringWithFormat:@"%@",testArray[i]];
if ([string rangeOfString:@"window._version_"].location != NSNotFound ) {
H5VersionStr = [NSString stringWithFormat:@"%@",testArray[i]];
}
}
}
// stringByReplacingOccurrencesOfString:@" " withString:@""]
// stringByReplacingOccurrencesOfString:@"\n" withString:@""];
NSString *newH5VersionStr = [NSString stringWithFormat:@"window._version_ = '%@'",self.NVersionH5];
NSString *resultsStr = [strUrl stringByReplacingOccurrencesOfString:H5VersionStr withString:newH5VersionStr];
//路径存在与否
if (![fileManager fileExistsAtPath:indexPath]) {
//文件夹路径存在与否
[fileManager createDirectoryAtPath:indexPath withIntermediateDirectories:YES attributes:nil error:nil];
NSLog(@"Not Found");
NSLog(@"文件存在");
}
NSData *txtData = [resultsStr dataUsingEncoding:NSUTF8StringEncoding];
[fileManager createFileAtPath:indexPath contents:txtData attributes:nil];
// NSLog(@"解压成功!path = %@",path);
// self.promptContent.text = @"请重新启动APP来完成APP更新";
// self.confirmBtn.hidden = NO;
self.stateStr = @"YES";
[self confirmBtnClik:nil];
}else{
// dispatch_async(dispatch_get_main_queue(), ^{
// self.promptTitle.text = @"文件解压失败!";
// });
// self.promptContent.text = error.localizedDescription;
// NSLog(@"解压失败!---%@",error.localizedDescription);
// self.confirmBtn.hidden = NO;
self.stateStr = @"NO";
}
}
];
}
// 截取字符串方法封装
- (NSString *)withOriginalString:(NSString *)OriginalString subStringFrom:(NSString *)startString to:(NSString *)endString{
if ([OriginalString length]<=0) {
return @"";
}
NSRange startRange = [OriginalString rangeOfString:startString];
NSRange endRange = [OriginalString rangeOfString:endString];
NSRange range = NSMakeRange(startRange.location + startRange.length, endRange.location - startRange.location - startRange.length);
return [OriginalString substringWithRange:range];
}
#pragma mark - SSZipArchiveDelegate
- (void)zipArchiveWillUnzipArchiveAtPath:(NSString *)path zipInfo:(unz_global_info)zipInfo {
NSLog(@"将要解压%d",zipInfo.number_entry);
}
- (void)zipArchiveDidUnzipArchiveAtPath:(NSString *)path zipInfo:(unz_global_info)zipInfo unzippedPath:(NSString *)unzippedPat uniqueId:(NSString *)uniqueId {
NSLog(@"解压完成!");
}
@end