在项目开发过程中, 为了实现热修复, 项目中集成了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 <Foundation/Foundation.h>
@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 JSPatchHandler
static 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类, 没什么好说的.
相关文件下载地址: https://github.com/hell03W/Files/tree/master/Files/JSPatchHandler
欢迎关注github.
如果您有更好的解决方案, 或者发现任何问题, 请联系: weidf@163.com