iOS 如何用原生代码实现app的热更新,且能顺利通过app store审核

目前,流行的热更新框架绝大多数都被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

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要更新 UniApp 开发的 iOS 应用,你需要执行以下步骤: 1. 更新应用代码:首先,你需要更新应用的代码。在 UniApp 项目中,你可以使用 HBuilderX 或其他支持 UniApp 的开发工具进行开发和调试。当你对应用进行修改后,确保代码已经更新到最新版本。 2. 生成 iOS 项目:UniApp 提供了将应用打包成原生应用的功能。在 HBuilderX 中,你可以选择 "发行" -> "原生App-云打包",根据提示选择 iOS 平台,然后点击 "云端打包"。这将生成一个用于 iOS 平台的项目文件夹。 3. 使用 Xcode 进行更新:打开生成的 iOS 项目文件夹,找到后缀为 `.xcworkspace` 的文件,双击打开项目。确保你已经安装了 Xcode 开发工具。 4. 更新应用版本号:在 Xcode 中,选择项目导航栏中的项目文件,在 "General" 选项卡中找到 "Version" 和 "Build" 设置。根据你的需要,更新应用的版本号和构建号。 5. 构建和签名应用:选择 Xcode 中的 "Product" 菜单,然后选择 "Archive"。Xcode 将构建应用,并将其打包成一个 `.ipa` 文件。 6. 分发和更新:将生成的 `.ipa` 文件发布到 App Store Connect(前身为 iTunes Connect)中。在 App Store Connect 中,你可以管理你的应用、配置更新和发布新版本。用户可以通过 App Store 接收到更新通知并进行更新。 需要注意的是,以上步骤仅适用于 iOS 平台。如果你的应用还需要支持其他平台,比如 Android 平台,你需要相应的开发工具和发布流程来更新和发布应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值