Authored by 盖剑秋

Add auto update framework. Reviewer: Yu Liang.

... ... @@ -14,7 +14,7 @@ target 'YH_Vendor' do
], :path => '../node_modules/react-native'
pod 'RNDeviceInfo', :path => '../node_modules/react-native-device-info'
pod 'SSZipArchive', '~> 1.2'
end
# Start the React Native JS packager server when running the project in Xcode.
... ...
... ... @@ -21,6 +21,7 @@ PODS:
- React/Core
- RNDeviceInfo (0.9.2):
- React
- SSZipArchive (1.2)
DEPENDENCIES:
- React/Core (from `../node_modules/react-native`)
... ... @@ -33,6 +34,7 @@ DEPENDENCIES:
- React/RCTVibration (from `../node_modules/react-native`)
- React/RCTWebSocket (from `../node_modules/react-native`)
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
- SSZipArchive (~> 1.2)
EXTERNAL SOURCES:
React:
... ... @@ -43,5 +45,6 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
React: a764d67f6cf360723120951301cba3ee5ee05ce2
RNDeviceInfo: e3fe8d8fe52f74eab22b7d4784a4fdd2e9bf4a26
SSZipArchive: 251093c65f98d6ea282c50bc404bfa631d9fd721
COCOAPODS: 0.39.0
... ...
... ... @@ -12,6 +12,7 @@
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
2E572BF51D00089000B89A37 /* RNAutoUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E572BF41D00089000B89A37 /* RNAutoUpdater.m */; };
2EDB1CE11CFED97B001CA832 /* RNNativeConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 2EDB1CE01CFED97B001CA832 /* RNNativeConfig.m */; };
5AC1B39F4940B0FAC56474FA /* libPods-YH_Vendor.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B562CE3CB8E496AF9768090D /* libPods-YH_Vendor.a */; };
87AF818B1CFD959D00ACE834 /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 87AF818A1CFD959D00ACE834 /* libRNDeviceInfo.a */; };
... ... @@ -40,6 +41,8 @@
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = YH_Vendor/Images.xcassets; sourceTree = "<group>"; };
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = YH_Vendor/Info.plist; sourceTree = "<group>"; };
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = YH_Vendor/main.m; sourceTree = "<group>"; };
2E572BF31D00089000B89A37 /* RNAutoUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNAutoUpdater.h; sourceTree = "<group>"; };
2E572BF41D00089000B89A37 /* RNAutoUpdater.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNAutoUpdater.m; sourceTree = "<group>"; };
2EDB1CDF1CFED97B001CA832 /* RNNativeConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNNativeConfig.h; sourceTree = "<group>"; };
2EDB1CE01CFED97B001CA832 /* RNNativeConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNativeConfig.m; sourceTree = "<group>"; };
5B51D3B6F0CB3E5047D881CA /* Pods-YH_Vendor.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-YH_Vendor.debug.xcconfig"; path = "Pods/Target Support Files/Pods-YH_Vendor/Pods-YH_Vendor.debug.xcconfig"; sourceTree = "<group>"; };
... ... @@ -94,6 +97,7 @@
13B07FB61A68108700A75B9A /* Info.plist */,
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
13B07FB71A68108700A75B9A /* main.m */,
2E572BF21D00089000B89A37 /* RNAutoUpdater */,
2EDB1CDE1CFED96B001CA832 /* RNNativeConfig */,
);
name = YH_Vendor;
... ... @@ -108,6 +112,16 @@
name = Pods;
sourceTree = "<group>";
};
2E572BF21D00089000B89A37 /* RNAutoUpdater */ = {
isa = PBXGroup;
children = (
2E572BF31D00089000B89A37 /* RNAutoUpdater.h */,
2E572BF41D00089000B89A37 /* RNAutoUpdater.m */,
);
name = RNAutoUpdater;
path = YH_Vendor/RNAutoUpdater;
sourceTree = "<group>";
};
2EDB1CDE1CFED96B001CA832 /* RNNativeConfig */ = {
isa = PBXGroup;
children = (
... ... @@ -327,6 +341,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2E572BF51D00089000B89A37 /* RNAutoUpdater.m in Sources */,
2EDB1CE11CFED97B001CA832 /* RNNativeConfig.m in Sources */,
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
... ...
//
// RNAutoUpdater.h
// YH_Vendor
//
// Created by 盖剑秋 on 16/6/2.
// Copyright © 2016年 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, INSTALL_MODE) {
INSTALL_MODE_IMMEDIATE = 1, //立即加载
INSTALL_MODE_ON_NEXT_START = 2, //下次重新启动时加载
INSTALL_MODE_ON_NEXT_RESUME = 3, //下次进入前台时加载
};
@interface RNAutoUpdater : NSObject
@end
... ...
//
// RNAutoUpdater.m
// YH_Vendor
//
// Created by 盖剑秋 on 16/6/2.
// Copyright © 2016年 Facebook. All rights reserved.
//
#import "RNAutoUpdater.h"
#import "RCTBridgeModule.h"
#import <CommonCrypto/CommonDigest.h>
#import "SSZipArchive.h"
#ifdef DEBUG
# define DLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__);
#else
# define DLog(...)
#endif
#define kYH_JSBundleVersion(appVersion) [NSString stringWithFormat:@"YH_JSBundleVersion_%@", appVersion]
#define kRNBundleURL @"http://m.yohobuy.com/rn/v1"
#define kFileMD5Key @"yohorn2016"
typedef void (^YH_UpdateCallback)(NSError *error);
typedef enum {
YH_UpdateErrorHTTPStatusFailed = -1001,
YH_UpdateErrorResponseDataFailed,
YH_UpdateErrorFetchFileFailed,
YH_UpdateErrorVerifyFailed,
YH_UpdateErrorUnzipFailed,
YH_UpdateErrorCannotFindJSBundle,
} YH_UpdateError;
@interface RNAutoUpdater()<RCTBridgeModule>
@end
@implementation RNAutoUpdater
RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(checkUpdate:(NSDictionary *)options){
INSTALL_MODE mode = [[options objectForKey:@"installMode"] integerValue];
[[self class] checkUpdateWithCallback:^(NSError *error) {
}];
}
- (NSDictionary *)constantsToExport {
return @{
@"jsBundleVersion":[[self class] currentVersion],
@"INSTALL_MODE_IMMEDIATE":@(INSTALL_MODE_IMMEDIATE),
@"INSTALL_MODE_ON_NEXT_START":@(INSTALL_MODE_ON_NEXT_START),
@"INSTALL_MODE_ON_NEXT_RESUME":@(INSTALL_MODE_ON_NEXT_RESUME),
};
}
+ (void)checkUpdate
{
[self checkUpdateWithCallback:^(NSError *error) {
if (!error) {
}
}];
}
+ (NSURL *)currentJSBundleLocation
{
NSURL *bundleURL;
NSString *version = [self currentVersion];
if ([version isEqualToString:@""]) {
bundleURL = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
} else {
NSString *scriptDirectory = [self fetchScriptDirectory];
NSString *fileName = [self fileName:[self currentVersion]];
NSString *filePath = [scriptDirectory stringByAppendingPathComponent:fileName];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
bundleURL = [NSURL URLWithString:filePath];
} else {
bundleURL = nil;
}
}
return bundleURL;
}
+ (NSMutableURLRequest *)createCheckUpdateRequest
{
//create url request
NSURL *hotfixURL = [NSURL URLWithString:kRNBundleURL];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:hotfixURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:20.0];
mutableRequest.HTTPMethod = @"POST";
NSString *appVersion = [self containerVersion];
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
parameters[@"app_version"] = appVersion;
parameters[@"client_type"] = @"iOS";
NSMutableArray *pairs = [NSMutableArray array];
for (NSString *key in [parameters allKeys]) {
NSString *value = parameters[key];
[pairs addObject:[NSString stringWithFormat:@"%@=%@", key, value]];
}
NSString *bodyString = [pairs componentsJoinedByString:@"&"];
[mutableRequest setHTTPBody:[bodyString dataUsingEncoding:NSUTF8StringEncoding]];
return mutableRequest;
}
+ (void)checkUpdateWithCallback:(YH_UpdateCallback)callback
{
DLog(@"YH_JSBundle: checkUpdate");
DLog(@"YH_JSBundle: request file %@", kRNBundleURL);
NSMutableURLRequest *mutableRequest = [self createCheckUpdateRequest];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:mutableRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
DLog(@"YH_JSBundle: request RNBundleURL failure, error:%@", error);
if (callback) {
callback(error);
}
return;
}
//解析json
NSError *jsonError;
NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
if (!result) {
DLog(@"YH_JSBundle: unrecongized json string, error:%@", jsonError);
if (callback) {
callback(jsonError);
}
return;
}
DLog(@"YH_JSBundle: request RNBundleURL success, response data:%@", result);
//接口数据处理
NSInteger code = [result[@"code"] integerValue];
if (code != 200) {
DLog(@"YH_JSBundle: response status error, code:%ld", code);
if (callback) {
callback([NSError errorWithDomain:@"com.yohobuy" code:YH_UpdateErrorHTTPStatusFailed userInfo:nil]);
}
return;
}
NSDictionary *bundleData = result[@"data"];
NSString *zipURLString = bundleData[@"url"];
NSString *bundleVersion = bundleData[@"rnv"];
NSString *fileMD5 = bundleData[@"filecode"];
NSString *minContainerVersion = bundleData[@"minv"];
//获取脚本
[self fetchPatchFile:zipURLString version:bundleVersion md5:fileMD5 minContainerVersion:minContainerVersion callback:callback];
}];
[task resume];
}
+ (void)fetchPatchFile:(NSString *)fileURLString version:(NSString *)version md5:(NSString *)fileMD5 minContainerVersion:(NSString *)minContainerVersion callback:(YH_UpdateCallback)callback
{
if ([fileURLString isEqualToString:@""] || [version isEqualToString:@""] || [fileMD5 isEqualToString:@""]) {
DLog(@"YH_JSBundle: fetch js bundle file fail, url:%@, version:%@, md5:%@", fileURLString, version, fileMD5);
if (callback) {
callback([NSError errorWithDomain:@"com.yohobuy" code:YH_UpdateErrorResponseDataFailed userInfo:nil]);
}
return;
}
BOOL shouldDownload = [self shouldDownloadUpdateJSBundle:version forMinContainerVersion:minContainerVersion];
if (!shouldDownload) {
DLog(@"YH_JSBundle: js bundle file already exist");
return;
}
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:fileURLString] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:20.0];
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
DLog(@"YH_JSBundle: fetch patch file fail, error:%@", error);
if (callback) {
callback([NSError errorWithDomain:@"com.yohobuy" code:YH_UpdateErrorFetchFileFailed userInfo:nil]);
}
return;
}
NSString *appVersion = [self containerVersion];
//脚本临时存放路径
NSString *downloadTmpPath = [NSString stringWithFormat:@"%@jsbundle_%@_%@.zip", NSTemporaryDirectory(), appVersion, version];
NSString *unzipTmpDirectory = [NSString stringWithFormat:@"%@jsbundle_%@_%@_unzip/", NSTemporaryDirectory(), appVersion, version];
[data writeToFile:downloadTmpPath atomically:YES];
//验证文件签名
BOOL pass = [self verifyFileSign:downloadTmpPath realSign:fileMD5];
if (!pass) {
DLog(@"YH_JSBundle: file is broken");
//清除临时文件
[[NSFileManager defaultManager] removeItemAtPath:downloadTmpPath error:nil];
if (callback) {
callback([NSError errorWithDomain:@"com.yohobuy" code:YH_UpdateErrorVerifyFailed userInfo:nil]);
}
return;
}
//解压zip
BOOL success = [SSZipArchive unzipFileAtPath:downloadTmpPath toDestination:unzipTmpDirectory];
if (!success) {
DLog(@"YH_JSBundle: unzip file error");
[[NSFileManager defaultManager] removeItemAtPath:unzipTmpDirectory error:nil];
if (callback) {
callback([NSError errorWithDomain:@"com.yohobuy" code:YH_UpdateErrorUnzipFailed userInfo:nil]);
}
return;
}
//判断zip中是否存在 main.jsbundle
NSString *bundleTmpPath = [unzipTmpDirectory stringByAppendingPathComponent:@"main.jsbundle"];
if (![[NSFileManager defaultManager] fileExistsAtPath:bundleTmpPath]) {
DLog(@"YH_JSBundle: can not find js bundle in zip");
[[NSFileManager defaultManager] removeItemAtPath:unzipTmpDirectory error:nil];
if (callback) {
callback([NSError errorWithDomain:@"com.yohobuy" code:YH_UpdateErrorCannotFindJSBundle userInfo:nil]);
}
return;
}
//存储js bundle
NSString *scriptDirectory = [self fetchScriptDirectory];
[[NSFileManager defaultManager] createDirectoryAtPath:scriptDirectory withIntermediateDirectories:YES attributes:nil error:nil];
NSString *fileName = [self fileName:version];
NSString *newFilePath = [scriptDirectory stringByAppendingPathComponent:fileName];
[[NSData dataWithContentsOfFile:bundleTmpPath] writeToFile:newFilePath atomically:YES];
DLog(@"YH_JSBundle: fetch patch file success, path:%@", newFilePath);
//存储最新patch版本号
[[NSUserDefaults standardUserDefaults] setObject:version forKey:kYH_JSBundleVersion(appVersion)];
[[NSUserDefaults standardUserDefaults] synchronize];
if (callback) {
callback(nil);
}
//清除临时文件和目录
[[NSFileManager defaultManager] removeItemAtPath:downloadTmpPath error:nil];
[[NSFileManager defaultManager] removeItemAtPath:unzipTmpDirectory error:nil];
}];
[task resume];
}
+ (BOOL)shouldDownloadUpdateJSBundle:(NSString *)bundleVersion forMinContainerVersion:(NSString *)minContainerVersion
{
NSString *containerVersion = [self containerVersion];
//应用版本号 < bundle要求的最低版本号,不下载更新
if ([containerVersion compare:minContainerVersion options:NSNumericSearch] == NSOrderedAscending) {
return NO;
}
//本地bundle版本号 == 最新bundle版本号,不下载更新
if ([[self currentVersion] isEqualToString:bundleVersion]) {
return NO;
}
return YES;
}
+ (BOOL)verifyFileSign:(NSString *)filePath realSign:(NSString *)realSign
{
NSString *fileSign = [self fileMD5:filePath];
NSString *joinString = [NSString stringWithFormat:@"%@%@", fileSign, kFileMD5Key];
NSString *actualSign = [self stringMD5:joinString];
if ([actualSign isEqualToString:realSign]) {
return YES;
} else {
return NO;
}
}
+ (NSString *)fileName:(NSString *)version
{
NSString *name = [NSString stringWithFormat:@"main_%@.jsbundle", version];
return name;
}
+ (NSString *)containerVersion
{
return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
}
+ (NSString *)currentVersion
{
NSString *appVersion = [self containerVersion];
NSString *version = [[NSUserDefaults standardUserDefaults] stringForKey:kYH_JSBundleVersion(appVersion)];
return version ?: @"";
}
+ (NSString *)fetchScriptDirectory
{
NSString *appVersion = [self containerVersion];
NSString *libraryDirectory = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];
NSString *scriptDirectory = [libraryDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"YH_JSBundle/%@/", appVersion]];
return scriptDirectory;
}
#pragma mark utils
+ (NSString *)fileMD5:(NSString *)filePath
{
NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:filePath];
if(!handle)
{
return nil;
}
CC_MD5_CTX md5;
CC_MD5_Init(&md5);
BOOL done = NO;
while (!done)
{
NSData *fileData = [handle readDataOfLength:256];
CC_MD5_Update(&md5, [fileData bytes], (CC_LONG)[fileData length]);
if([fileData length] == 0)
done = YES;
}
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5_Final(digest, &md5);
NSString *result = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
digest[0], digest[1],
digest[2], digest[3],
digest[4], digest[5],
digest[6], digest[7],
digest[8], digest[9],
digest[10], digest[11],
digest[12], digest[13],
digest[14], digest[15]];
return result;
}
+ (NSString *)stringMD5:(NSString *)rawString
{
const char *input = [rawString UTF8String];
unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5(input, (CC_LONG)strlen(input), result);
NSMutableString *digest = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for (NSInteger i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[digest appendFormat:@"%02x", result[i]];
}
return digest;
}
@end
... ...
'use strict';
// let RNAutoUpdater = require('react-native').NativeModules.RNAutoUpdater;
let RNAutoUpdater = require('react-native').NativeModules.RNAutoUpdater;
const INSTALL_MODE_IMMEDIATE = 1; //立即加载
const INSTALL_MODE_ON_NEXT_START = 2; //下次重新启动时加载
... ... @@ -15,12 +15,12 @@ function checkUpdate(options) {
...options
}
// RNAutoUpdater.checkUpdate(options);
RNAutoUpdater.checkUpdate(options);
}
function jsBundleVersion() {
// return RNAutoUpdater.jsBundleVersion;
return RNAutoUpdater.jsBundleVersion;
}
module.exports = {
... ...