iOS 動態熱修補 JSPatch

簡介: 所謂動態熱修補就是把可以致使app 崩潰的嚴重bug,提交新版本到appstore 審覈速度太慢影響用戶使用,這時候就能夠利用 JSPatch 可讓你用 JavaScript 書寫原生 iOS APP。

所謂動態熱修補就是把可以致使app 崩潰的嚴重bug,提交新版本到appstore 審覈速度太慢影響用戶使用,這時候就能夠利用git

JSPatch 可讓你用 JavaScript 書寫原生 iOS APP。只需在項目引入極小的引擎,就可使用 JavaScript 調用任何 Objective-C 的原生接口,得到腳本語言的優點:爲項目動態添加模塊,或替換項目原生代碼動態修復 bug。github

這裏就不在贅述優缺點重點看實現!面試

(一)首先在終端swift

pod search JSPatch

接下來就能夠本身pod進你須要引入的工程中,或者download到本地本身加到工程中搜索結果以下數組

-> JSPatch (0.1.4)
  JSPatch bridge Objective-C and JavaScript. You can call any Objective-C class
  and method in JavaScript by just including a small engine.
  pod 'JSPatch', '~> 0.1.4'
  - Homepage: https://github.com/bang590/JSPatch
  - Source: https://github.com/bang590/JSPatch.git
  - Versions: 0.1.4, 0.1.3, 0.1.2, 0.1.1, 0.1, 0.0.3, 0.0.2, 0.0.1 [master
  repo] - 0.1.4, 0.1.3, 0.1.2, 0.1.1, 0.1, 0.0.3, 0.0.2, 0.0.1 [master-1 repo]
  - Subspecs:
   - JSPatch/Core (0.1.4)
   - JSPatch/Extensions (0.1.4)
macxy:~ xy$

通常咱們將執行的".js"文件放在服務器端每次程序啓動時候能夠執行比對本地js文件與服務器端js文件是否有變化這裏細節就不作解釋服務器

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
   // 加載引擎
  [JPEngine startEngine];
//  本地JS,動態更新技術就是經過服務器獲取JS更新這個JS
  NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
  NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];
  [JPEngine evaluateScript:script];
  從服務器獲取更新JS
  [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://cnbang.net/test.js"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
  NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
   //執行js
  [JPEngine evaluateScript:script];
  }];
}

這是大概過程舉個小栗子描述下js替換的具體應用;app

先上一段js文件解釋下oc與js交互如何訪問oc APIjsp

//按鈕事件
defineClass('YFJSPatchMainViewController', {//哪一個類中的方法
  buttonTouch: function(button) {//方法名:function(參數)
//跳轉到tableView
    var tableViewCtrl = YFJSPatchTsetViewController.alloc().init()
self.navigationController().pushViewController_animated(tableViewCtrl, YES) } })

相信你們也都看的懂那麼咱們要怎麼去用呢好比 我上面所寫的就是要找到async

YFJSPatchMainViewController這個類中的

-(void)buttonTouch:(uibutton)btn
{
}
這個方法最開始咱們什麼都沒有寫,當執行完js文件後這個方法就會去執行js  而後push;原理就是這樣要本身去用一下才好祝您愉快

  1. require

在使用Objective-C類以前須要調用 require('className’) :ide

require('UIView')
var view = UIView.alloc().init()

能夠用逗號 , 分隔,一次性導入多個類:

require('UIView, UIColor')
var view = UIView.alloc().init()
var red = UIColor.redColor()

或者直接在使用時才調用 require() :

require('UIView').alloc().init()

  1. 調用OC方法

調用類方法

var redColor = UIColor.redColor();

調用實例方法

var view = UIView.alloc().init();
view.setNeedsLayout();

參數傳遞

跟在OC同樣傳遞參數:

var view = UIView.alloc().init();
var superView = UIView.alloc().init()
superView.addSubview(view)

Property

獲取/修改 Property 等於調用這個 Property 的 getter / setter 方法,獲取時記得加 ():

view.setBackgroundColor(redColor);
var bgColor = view.backgroundColor();

方法名轉換

多參數方法名使用 _ 分隔:

var indexPath = require('NSIndexPath').indexPathForRow_inSection(0, 1);

若原 OC 方法名裏包含下劃線 _,在 JS 使用雙下劃線 __ 代替:

// Obj-C: [JPObject _privateMethod];
JPObject.__privateMethod()

  1. defineClass

API

defineClass(classDeclaration, [properties,] instanceMethods, classMethods)

@param classDeclaration: 字符串,類名/父類名和Protocol
@param properties: 新增property,字符串數組,可省略
@param instanceMethods: 要添加或覆蓋的實例方法
@param classMethods: 要添加或覆蓋的類方法

覆蓋方法

1.在 defineClass 裏定義 OC 已存在的方法便可覆蓋,方法名規則與調用規則同樣,使用 _ 分隔:

// OC
@implementation JPTestObject
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}
@end

// JS
defineClass("JPTableViewController", {
  tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
    ...
  },
})

2.使用雙下劃線 __ 表明原OC方法名裏的下劃線 _ :

// OC
@implementation JPTableViewController
- (NSArray *) _dataSource {
}
@end

// JS
defineClass("JPTableViewController", {
  __dataSource: function() {
  },
})

3.在方法名前加 ORIG 便可調用未覆蓋前的 OC 原方法:

// OC
@implementation JPTableViewController
- (void)viewDidLoad {
}
@end

// JS
defineClass("JPTableViewController", {
  viewDidLoad: function() {
     self.ORIGviewDidLoad();
  },
})

覆蓋類方法

defineClass() 第三個參數就是要添加或覆蓋的類方法,規則與上述覆蓋實例方法一致:

// OC
@implementation JPTestObject
+ (void)shareInstance
{
}
@end

// JS
defineClass("JPTableViewController", {
  //實例方法
}, {
  //類方法
  shareInstance: function() {
    ...
  },
})

覆蓋 Category 方法

覆蓋 Category 方法與覆蓋普通方法同樣:

@implementation UIView (custom)
- (void)methodA {
}
+ (void)clsMethodB {
}
@end

defineClass('UIView', {
  methodA: function() {
  }
}, {
  clsMethodB: function() {
  }
});

Super

使用 self.super() 接口表明 super 關鍵字,調用 super 方法:

// JS
defineClass("JPTableViewController", {
  viewDidLoad: function() {
     self.super().viewDidLoad();
  }
})

Property

獲取/修改 OC 定義的 Property

用調用 getter / setter 的方式獲取/修改已在 OC 定義的 Property:

// OC
@interface JPTableViewController
@property (nonatomic) NSArray *data;
@property (nonatomic) NSString *shareURL;
@property (nonatomic) NSString *shareTitle;
@end
@implementation JPTableViewController
@end

// JS
defineClass("JPTableViewController", {
  viewDidLoad: function() {
     var data = self.data();     //get property value
     self.setData(data.toJS().push("JSPatch"));     //set property value
     var sel = self;
     self.bridge().registerHandler_handler('h5ToNativeShareDialog', block('NSDictionary *',function(data,responseCallback) {         
         sel.setShareURL(data.objectForKey('url'));
         sel.setShareTitle(data.objectForKey('title'));
     }));
})

動態新增 Property

能夠在 defineClass() 第二個參數爲類新增 property,格式爲字符串數組,使用時與 OC property 接口一致:

defineClass("JPTableViewController", ['data', 'totalCount'], {
  init: function() {
     self = self.super().init()
     self.setData(["a", "b"])     //添加新的 Property (id data)
     self.setTotalCount(2)
     return self
  },
  viewDidLoad: function() {
     var data = self.data()     //獲取 Property 值
     var totalCount = self.totalCount()
  },
})

私有成員變量

使用 valueForKey()setValue_forKey() 獲取/修改私有成員變量:

// OC
@implementation JPTableViewController {
     NSArray *_data;
}
@end

// JS
defineClass("JPTableViewController", {
  viewDidLoad: function() {
     var data = self.valueForKey("_data")     //get member variables
     self.setValue_forKey(["JSPatch"], "_data")     //set member variables
  },
})

添加新方法

能夠給一個類隨意添加 OC 未定義的方法,但全部的參數類型都是 id:

// OC
@implementation JPTableViewController
- (void)viewDidLoad
{
     NSString* data = [self dataAtIndex:@(1)];
     NSLog(@"%@", data);      //output: Patch
}
@end

// JS
var data = ["JS", "Patch"]
defineClass("JPTableViewController", {
  dataAtIndex: function(idx) {
     return idx < data.length ? data[idx]: ""
  }
})

若新增的方法屬於 Protocol 裏的接口,須要在 defineClass 的類聲明參數裏指明實現的 Protocol,詳見下文。

Protocol

能夠在定義時讓一個類實現某些 Protocol 接口,寫法跟 OC 同樣:

defineClass("JPViewController: UIViewController<UIScrollViewDelegate, UITextViewDelegate>", {

})

這樣作的做用是,當添加 Protocol 裏定義的方法,而類裏沒有實現的方法時,參數類型再也不全是 id,而是自動轉爲 Protocol 裏定義的類型:

@protocol UIAlertViewDelegate <NSObject>
...
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex;
...
@end

defineClass("JPViewController: UIViewController <UIAlertViewDelegate>", {
  viewDidAppear: function(animated) {
    var alertView = require('UIAlertView')
      .alloc()
      .initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles(
        "Alert",
        self.dataSource().objectAtIndex(indexPath.row()), 
        self, 
        "OK", 
        null
      )
     alertView.show()
  }
  alertView_clickedButtonAtIndex: function(alertView, buttonIndex) {
    console.log('clicked index ' + buttonIndex)
  }
})

  1. 特殊類型

Struct

JSPatch原生支持 CGRect / CGPoint / CGSize / NSRange 這四個 struct 類型,用 JS 對象表示:

// Obj-C
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 100, 100)];
[view setCenter:CGPointMake(10,10)];
[view sizeThatFits:CGSizeMake(100, 100)];
CGFloat x = view.frame.origin.x;

NSRange range = NSMakeRange(0, 1);

// JS
var view = UIView.alloc().initWithFrame({x:20, y:20, width:100, height:100})
view.setCenter({x: 10, y: 10})
view.sizeThatFits({width: 100, height:100})

var x = view.frame().x
var range = {location: 0, length: 1}

其餘 Struct 類型的支持請參照 添加 struct 類型支持

Selector

在JS使用字符串表明 Selector:

//Obj-C
[self performSelector:@selector(viewWillAppear:) withObject:@(YES)];

//JS
self.performSelector_withObject("viewWillAppear:", 1)

nil

JS 上的 nullundefined 都表明 OC 的 nil,若是要表示 NSNull, 用 nsnull 代替,若是要表示 NULL, 也用 null 代替:

//Obj-C
@implemention JPTestObject
+ (BOOL)testNull(NSNull *null) {
    return [null isKindOfClass:[NSNull class]]
}
@end

//JS
require('JPTestObject').testNull(nsnull) //return 1
require('JPTestObject').testNull(null) //return 0

在JS裏面判斷是否爲空要判斷false:

var url = "";
var rawData = NSData.dataWithContentsOfURL(NSURL.URLWithString(url));
if (rawData != null) {} //這樣判斷是錯誤的
應該以下判斷:
if (!rawData){}
在JSPatch.js源碼裏_formatOCToJS方法對undefined,null,isNil轉換成了false。

  1. NSArray / NSString / NSDictionary

NSArray / NSString / NSDictionary 不會自動轉成對應的JS類型,像普通 NSObject 同樣使用它們:

//Obj-C
@implementation JPObject
+ (NSArray *)data
{
  return @[[NSMutableString stringWithString:@"JS"]]
}
+ (NSMutableDictionary *)dict
{
    return [[NSMutableDictionary alloc] init];
}
@end

// JS
require('JPObject')
var ocStr = JPObject.data().objectAtIndex(0)
ocStr.appendString("Patch")

var dict = JPObject.dict()
dict.setObject_forKey(ocStr, 'name')
console.log(dict.objectForKey('name')) //output: JSPatch

若是要把 NSArray / NSString / NSDictionary 轉爲對應的 JS 類型,使用 .toJS() 接口:

// JS
var data = require('JPObject').data().toJS()
//data instanceof Array === true
data.push("Patch")

var dict = JPObject.dict()
dict.setObject_forKey(data.join(''), 'name')
dict = dict.toJS()
console.log(dict['name'])    //output: JSPatch

  1. Block

block 傳遞

當要把 JS 函數做爲 block 參數給 OC時,須要先使用 block(paramTypes, function) 接口包裝:

// Obj-C
@implementation JPObject
+ (void)request:(void(^)(NSString *content, BOOL success))callback
{
  callback(@"I'm content", YES);
}
@end

// JS
require('JPObject').request(block("NSString *, BOOL", function(ctn, succ) {
  if (succ) log(ctn)  //output: I'm content
}))

這裏 block 裏的參數類型用字符串表示,寫上這個 block 各個參數的類型,用逗號分隔。NSObject 對象如 NSString *, NSArray *等能夠用 id 表示,但 block 對象要用 NSBlock* 表示。

從 OC 返回給 JS 的 block 會自動轉爲 JS function,直接調用便可:

// Obj-C
@implementation JPObject
typedef void (^JSBlock)(NSDictionary *dict);
+ (JSBlock)genBlock
{
  NSString *ctn = @"JSPatch";
  JSBlock block = ^(NSDictionary *dict) {
    NSLog(@"I'm %@, version: %@", ctn, dict[@"v"])
  };
  return block;
}
+ (void)execBlock:(JSBlock)blk
{
}
@end

// JS
var blk = require('JPObject').genBlock();
blk({v: "0.0.1"});  //output: I'm JSPatch, version: 0.0.1

若要把這個從 OC 傳過來的 block 再傳回給 OC,一樣須要再用 block() 包裝,由於這裏 blk 已是一個普通的 JS function,跟咱們上面定義的 JS function 沒有區別:

// JS
var blk = require('JPObject').genBlock();
blk({v: "0.0.1"});  //output: I'm JSPatch, version: 0.0.1
require('JPObject').execBlock(block("id", blk));

總結:JS 沒有 block 類型的變量,OC 的 block 對象傳到 JS 會變成 JS function,全部要從 JS 傳 block 給 OC 都須要用 block() 接口包裝。

block 裏使用 self 變量

在 block 裏沒法使用 self 變量,須要在進入 block 以前使用臨時變量保存它:

defineClass("JPViewController", {
  viewDidLoad: function() {
    var slf = self;
    require("JPTestObject").callBlock(block(function(){
      //`self` is not available here, use `slf` instead.
      slf.doSomething();
    });
  }
}

限制

從 JS 傳 block 到 OC,有兩個限制:

A. block 參數個數最多支持6個。(若須要支持更多,能夠修改源碼)
B. block 參數類型不能是 double / NSBlock / struct 類型。

另外不支持 JS 封裝的 block 傳到 OC 再傳回 JS 去調用(緣由見 issue #155):

- (void)callBlock:(void(^)(NSString *str))block {
}

defineClass('JPTestObject', {
    run: function() {
        self.callBlock(block('NSString*', function(str) {
            console.log(str);
        }));
    },
    callBlock: function(blk) {
        //blk 這個 block 是上面的 run 函數裏 JS 傳到 OC 再傳過來的,沒法調用。
        blk("test block");   
    }
});

  1. __weak / __strong

能夠在 JS 經過 __weak() 聲明一個 weak 變量,主要用於避免循環引用。

例如咱們在 OC 裏爲了不 block 致使的循環引用,常常這樣寫:

- (void)test {
    __weak id weakSelf = self;
    [self setCompleteBlock:^(){
        [weakSelf blabla];
    }]
}

在 JS 對應的能夠這樣寫:

var weakSelf = __weak(self)
self.setCompleteBlock(block(function(){
    weakSelf.blabla();
}))

若要在使用 weakSelf 時把它變成 strong 變量,能夠用 __strong() 接口:

var weakSelf = __weak(self)
self.setCompleteBlock(block(function(){
    var strongSelf = __strong(weakSelf)
    strongSelf.blabla();
}))

  1. GCD

使用 dispatch_after() dispatch_async_main() dispatch_sync_main() dispatch_async_global_queue() 接口調用GCD方法:

// Obj-C
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  // do something
});

dispatch_async(dispatch_get_main_queue(), ^{
  // do something
});

// JS
dispatch_after(1.0, function(){
  // do something
})
dispatch_async_main(function(){
  // do something
})
dispatch_sync_main(function(){
  // do something
})
dispatch_async_global_queue(function(){
  // do something
})

  1. 傳遞 id* 參數

若是你須要傳遞 id* 參數,像 NSURLConnection 裏的這個接口裏的 NSError **:

+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error;

這裏傳入的是一個指向 NSObject 對象的指針,在方法裏能夠修改這個指針指向的對象,調用後外部能夠拿到新指向的對象,對於這樣的參數,首先須要引入 JPMemory 擴展,而後按如下步驟進行傳遞和獲取:

  1. 使用 malloc(sizeof(id)) 建立一個指針
  2. 把指針做爲參數傳給方法
  3. 方法調用完,使用 pval() 拿到指針新指向的對象
  4. 使用完後調用 releaseTmpObj() 釋放這個對象
  5. 使用 free() 釋放指針

舉個例子:

//OC
- (void)testPointer:(NSError **)error {
    NSError *err = [[NSError alloc]initWithDomain:@"com.jspatch" code:42 userInfo:nil];
    *error = err;
}

//JS
//malloc() pval() free() is provided by JPMemory extension
require('JPEngine').addExtensions(['JPMemory'])

var pError = malloc(sizeof("id"))
self.testPointer(pError)
var error = pval(pError)
if (!error) {
    console.log("success")
} else {
    console.log(error)
}
releaseTmpObj(pError)
free(pError)

若反過來你想在 JS 替換上述 -testPointer: 方法,構建 NSError 對象賦給傳進來的指針,能夠這樣寫:

defineClass('JPClassName', {
    testPointer: function(error){
        var  tmp = require('NSError').errorWithDomain_code_userInfo("test", 1, null);
        var newErrorPointer = getPointer(tmp)
        memcpy(error, newErrorPointer, sizeof('id'))
    }
);

  1. 常量、枚舉、宏、全局變量

常量/枚舉

Objective-C 裏的常量/枚舉不能直接在 JS 上使用,能夠直接在 JS 上用具體值代替:

//OC
[btn addTarget:self action:@selector(handleBtn) forControlEvents:UIControlEventTouchUpInside];

//UIControlEventTouchUpInside的值是1<<6
btn.addTarget_action_forControlEvents(self, "handleBtn", 1<<6);

或者在 JS 上從新定義同名的全局變量:

//js
var UIControlEventTouchUpInside  = 1 << 6;
btn.addTarget_action_forControlEvents(self, "handleBtn", UIControlEventTouchUpInside);

有些常量字符串,須要在 OC 用 NSLog 打出看看它的值是什麼:

//OC
[[NSAttributedString alloc].initWithString:@"str" attributes:@{NSForegroundColorAttributeName: [UIColor redColor]];

上面代碼中 NSForegroundColorAttributeName 是一個靜態字符串常量,源碼裏看不出它的值,能夠先用 NSLog 打出它的值再直接寫在 JS 上:

//OC
NSLog(@"%@", NSForegroundColorAttributeName) //output 'NSColor'

NSAttributedString.alloc().initWithString_attributes("無效啊", {'NSColor': UIColor.redColor()});

獲取宏值

Objective-C 裏的宏一樣不能直接在 JS 上使用。若定義的宏是一個值,能夠在 JS 定義一樣的全局變量代替,若定義的宏是程序,能夠在JS展開宏:

#define TABBAR_HEIGHT 40
#define SCREEN_WIDTH [[UIScreen mainScreen] bounds].size.height
[view setWidth:SCREEN_WIDTH height:TABBAR_HEIGHT];

//JS
view.setWidth_height(UIScreen.mainScreen().bounds().height, 40);

若宏的值是某些在底層才能獲取到的值,例如 CGFLOAT_MIN,能夠經過在某個類或實例方法裏將它返回,或者用添加擴展的方式提供支持:

@implementation JPMacroSupport
+ (void)main:(JSContext *)context
{
  context[@"CGFLOAT_MIN"] = ^CGFloat() {
    return CGFLOAT_MIN;
  }
}
@end

require('JPEngine').addExtensions(['JPMacroSupport'])
var floatMin = CGFLOAT_MIN();

修改宏值

JSPatch 不支持修改宏的值,若要修改,須要替換全部使用到這個宏的方法。例如:

#define VIEW_HEIGHT 40
@implementation JPMethodDemo
+ (void)func
{
  UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, VIEW_HEIGHT)];
  ...
}
@end

//JS
var VIEW_HEIGHT_NEW = 20;
defineClass('JPMethodDemo', {
  func: function() {
    var view = UIView.alloc().initWithFrame({x:0, y:0, width:100, height:VIEW_HEIGHT_NEW});
    ...
  }
});

全局變量

在類裏定義的 static 全局變量沒法在 JS 上獲取到,若要在 JS 拿到這個變量,須要在 OC 有類方法或實例方法把它返回:

static NSString *name;
@implementation JPTestObject
+ (NSString *)name {
  return name;
}
@end

var name = JPTestObject.name() //拿到全局變量值

  1. Swift

使用 defineClass() 覆蓋 Swift 類時,類名應爲 項目名.原類名,例如項目 demo 裏用 Swift 定義了 ViewController 類,在 JS 覆蓋這個類方法時要這樣寫:

defineClass('demo.ViewController', {})

對於調用已在 swift 定義好的類,也是同樣:

require('demo.ViewController')

須要注意幾點:

  1. 只支持調用繼承自 NSObject 的 Swift 類
  2. 繼承自 NSObject 的 Swift 類,其繼承自父類的方法和屬性能夠在 JS 調用,其餘自定義方法和屬性一樣須要加 dynamic 關鍵字才行。
  3. 若方法的參數/屬性類型爲 Swift 特有(如 Character / Tuple),則此方法和屬性沒法經過 JS 調用。
  4. Swift 項目在 JSPatch 新增類與 OC 無異,能夠正常使用。
  5. 加載動態庫

對於 iOS 內置的動態庫,若原 APP 裏沒有加載,能夠經過如下方式動態加載,以加載 SafariServices.framework 爲例:

var bundle = NSBundle.bundleWithPath("/System/Library/Frameworks/SafariServices.framework");
bundle.load();

加載後就可使用 SafariServices.framework 了。

  1. 調試

可使用 console.log() 打印一個對象,做用至關於 NSLog(),會直接在 XCode 控制檯打出。

console.log() 支持任意參數,但不支持像 NSLog 這樣 NSLog(@"num:%f", 1.0) 的拼接:

var view = UIView.alloc().init();
var str = "test";
var num = 1;
console.log(view, str, num)
console.log(str + num);   //直接在JS拼接字符串

也能夠經過 Safari 的調試工具對 JS 進行斷點調試.

推薦👇:

若是你想一塊兒進階,瞭解最新面試行情不妨添加一下交流羣642363427
查看原文

相關文章
相關標籤/搜索