在项目开发过程中, 为了实现热修复, 项目中集成了JSPatch框架.
为了更好的集成, 对JSPatch相关的操作完全封装到了一个类里面. 思路如下:
1. 首先调用一个类方法, 作为检测是否需要更新的入口.2. 从服务端请求数据, 请求服务端的补丁文件和对应的key(md5加密字符串, 目的: 安全传输/检测文件内容是否有变化).3. 请求结果存储到本地数据库. 比对请求到的key, 和本地存储的key对比. 1> 本地key和服务端请求到的key比对成功: 读取本地文件, 并执行js文件. 2> 比对失败(文件内容发生变化/本地不存在): 重新下载文件到本地, 并执行下载的js文件. 3> 本地存在的垃圾文件, 服务器返回结果没有的文件, 不做任何处理. 4> 统一清理本地的垃圾文件(以前下载产生的, 当前已经不需要的).
加密思路:
服务端: 1. 对每个文件进行md5加密, 这样, 如果文件发生了任何变化, key值就会发生变化, 移动端通过key值判断, 文件是否发生改变. 2. 对1的加密结果, 拼接一个约定的token, 再次加密. 因为, 这些js文件是非常敏感的, 万一下载途中被黑客截取, 黑客就能得到app中所有的方法和属性, 是非常不安全的. 加密后, 就算中途被黑客截取, 黑客也无法更改js文件, 从而, 移动端只执行从自己服务端下载的源文件.移动端: 1. 使用服务端完全相同的加密方法, 再次加密, 最后比对加密结果, 如果相同, 则执行对应js文件, 如果不同, 不做任何操作.
封装的 JSPatchHandler.h
//// JSPathHandler.h// Elite//// Created by www.6dao.cc on 16/10/8.// Copyright © 2016年 ledao. All rights reserved.//#import@interface JSPatchHandler : NSObject/// 从服务器请求, 是否有新的补丁, 如果有新的, 则下载新的补丁, 然后打补丁+ (void)checkPatch;/// 不从服务器请求, 本地重新装载补丁文件+ (void)reloadingPatch;@end
JSPatchHandler.m
//// JSPathHandler.m// Elite//// Created by www.6dao.cc on 16/10/8.// Copyright © 2016年 ledao. All rights reserved.//#import "JSPatchHandler.h"#import "JSPatchModel.h"#import "WHFileManager.h"#import "JPEngine.h"/** 处理逻辑: 1. 先调用一个类方法, 然后从服务器请求, 服务器中所有的补丁列表和对应的key. 2. 然后和本地存储的key对比: 1> 本地有 服务器端没有任何变化的文件, 不做任何处理. 2> 对比,本地文件和服务器文件发生了改变, 重新下载, 重新加载. 修改前和修改后, 相同的方法名, 会覆盖吗? 预测可以覆盖. 待检验. 3> 服务端新增文件, 重新下载, 重新加载. */static NSString *PatchDir = @"patch";const static NSString *secrityCode = @"0QEGR9123590u1";@interface JSPatchHandler ()@property (nonatomic, strong) NSOperationQueue *queue;@property (nonatomic, strong) NSArray *executedArray;@end@implementation JSPatchHandlerstatic NSArray *dataArray;+ (instancetype)sharePatchHandler { static JSPatchHandler *patchHandler; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ patchHandler = [[JSPatchHandler alloc] init]; patchHandler.queue = [[NSOperationQueue alloc] init]; // 开启JPEngine [JPEngine startEngine]; }); return patchHandler;}/// 从服务器请求, 是否有新的补丁/* 1. 从服务器获取补丁列表, 保存到本地数据库. 2. 检查列表中每一条信息, 如果本地存在, 则直接读取执行, 如果本地不存在或者有变化, 从网络去读取. 3. 网络数据, 下载后, 一方面, 保存到指定目录, 另一方面, 执行js文件, 4. 检查, 数据库中没有的文件名, 本地存在的文件名, 依次删除. */+ (void)checkPatch { NSLog(@"___________________________________\n %@", NSHomeDirectory()); [AFNTool requestWithUrlString:@"app/file/listData" params:nil success:^(NSDictionary *response, BOOL success, NSString *code) { if (!success) { return ; } NSArray *patchArray = [AssignToObject customModel:@"JSPatchModel" fromArray:response[@"data"]]; // 请求结果 保存数据库中 [JSPatchModel delDataBaseTable]; [patchArray insertRecordFromArray]; // 检测并执行js代码 dispatch_async(dispatch_get_global_queue(0, 0), ^{ [[JSPatchHandler sharePatchHandler] checkAndExcute:patchArray]; }); // 删除本地存在的垃圾文件 dispatch_async(dispatch_get_global_queue(0, 0), ^{ [[JSPatchHandler sharePatchHandler] clearLocalData]; }); }]; }/// 不从服务器请求, 本地重新装载补丁文件+ (void)reloadingPatch { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSArray *patchArray = [JSPatchModel getAllRecod]; [[JSPatchHandler sharePatchHandler] checkAndExcute:patchArray]; });}/// 检查本地是够存在, 如果存在去执行, 不存在去下载- (void)checkAndExcute:(NSArray *)array { for (JSPatchModel *patchModel in array) { if ([self verificationData:patchModel]) { [self loadPatchFile:patchModel]; }else{ [self downloadPatchFile:patchModel]; } } }/// 下载js文件, 下载完成, 调用 loadingPatchFileWithModel: 加载js文件.- (void)downloadPatchFile:(JSPatchModel *)patchModel { NSString *filePath = [[WHFileManager cacheDirWithSubpath:PatchDir] stringByAppendingPathComponent:patchModel.name]; [WHFileManager deleteFile:filePath]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; NSURL *url = [NSURL URLWithString:patchModel.downPath]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; NSURLSessionDownloadTask *task = [manager downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) { return [NSURL fileURLWithPath:filePath]; } completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) { if (filePath) { [self loadPatchFile:patchModel]; } }]; [task resume]; }/// 根据 data 数据包, 加载jsPatch/// 如果data度去过, 会被保存在 model 的data属性中, 如果没有读取, 则需要去读取- (void)loadPatchFile:(JSPatchModel *)patchModel { if (![self verificationData:patchModel]) { return ; } NSString *script = [[NSString alloc] initWithData:patchModel.data encoding:NSUTF8StringEncoding]; [JPEngine evaluateScript:script];}/// 根据 参数model, 判断参数中数据是否经过自己服务器加密, 如果是, 则可以执行jsdiamante, 如果不是, 不能执行- (BOOL)verificationData:(JSPatchModel *)patchModel { NSString *filePath = [[WHFileManager cacheDirWithSubpath:PatchDir] stringByAppendingPathComponent:patchModel.name]; if (![WHFileManager fileExistsAtPath:filePath]) { return NO; } if (!patchModel.data) { patchModel.data = [WHFileManager readFile:filePath]; } NSString *fileString = [[NSString alloc] initWithData:patchModel.data encoding:NSUTF8StringEncoding]; NSString *secrityKey = [[NSString stringWithFormat:@"%@%@", [fileString md5String], secrityCode] md5String]; return [secrityKey isEqualToString:patchModel.fileKey];}#pragma mark - 有空 给FMDBHelper, 加上一个队列操作数据库, 保证操作的同步执行 FMDBDatabaseQueue// 检查并删除, 本地存在但是数据库中没有记录的 记录- (void)clearLocalData { NSString *filePath = [WHFileManager cacheDirWithSubpath:PatchDir]; // 获取数据库中记录 NSArray *records = [JSPatchModel getAllRecod]; NSArray *recordFileNames = [records valueForKeyPath:@"name"]; NSString *targetString = [recordFileNames componentsJoinedByString:@", "]; NSArray *localFiles = [WHFileManager contentsOfDirectoryAtPath:filePath]; for (NSString *fileName in localFiles) { if (![targetString containsString:fileName] && ![fileName hasPrefix:@"."]) { [WHFileManager deleteFile:[filePath stringByAppendingPathComponent:fileName]]; } }}@end
WHFileManager 是自己对文件操作的相关封装, 文件操作还是比较简单的, 只是不经常用到, 经常忘掉 ?, 封装一下, 以后拿来就用.
JSPatchModel是一个数据Model类, 没什么好说的.
相关文件下载地址:
欢迎关注github.
如果您有更好的解决方案, 或者发现任何问题, 请联系: