1. 程式人生 > >JSPatch原始碼剖析(一)

JSPatch原始碼剖析(一)

專案中使用到了JSPatch來實現線上APP bug的hot fix,使用後覺得JSPatch短小精悍並且功能強大,於是想往裡窺探下其實現機制。本文從JSPatch的使用角度去分析原始碼。

JavaScript執行環境建立

JavaScript執行環境的建立很簡單,只需要呼叫一個API即可:

[JPEngine startEngine];

+(void)startEngineAPI的主要工作是建立一個JSContext型別的全域性_context變數,後續JS和OC的互調都是在這個上下文中進行。由於JSContext實現了下面兩個方法,從而可以進行下標操作訪問:

- (JSValue
*)
objectForKeyedSubscript:(id)key; - (void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying> *)key;

_contenxt的初始化中,就大量的運用了下標操作符,就是下面這種方式:

context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
        return defineClass(classDeclaration, instanceMethods, classMethods);
    };
...

通過這種方式,以後就可以在JS中直接使用_OC_defineClass,從而呼叫到OC的defineClass函式。
在初始化最後,直接在_context中執行下jspatch.jsJS程式碼:

    NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"JSPatch" ofType:@"js"];
    NSAssert(path, @"can't find JSPatch.js");
    NSString *jsCore = [[NSString alloc] initWithData:[[NSFileManager defaultManager] contentsAtPath:path] encoding:NSUTF8StringEncoding];

    if ([_context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
        [_context evaluateScript:jsCore withSourceURL:[NSURL URLWithString:@"JSPatch.js"]];
    } else {
        [_context evaluateScript:jsCore];
    }

jspatch.js中在JS全域性上下文中定義了defineClassdefineProtocol等。

解析JS程式碼

下面是JSPatch Demo裡的一段JS原始碼,該demo通過JS寫了個UITableViewContorller的示例:

// demo.js
defineClass('JPTableViewController : UITableViewController <UIAlertViewDelegate>', {
  dataSource: function() {
    var data = self.getProp('data')
    ...
    self.setProp_forKey(data, 'data')
    return data;
  },
  numberOfSectionsInTableView: function(tableView) {},
  tableView_cellForRowAtIndexPath: function(tableView, indexPath) {},
  ...
})

通過呼叫+ (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)resourceURLAPI將該JS程式碼放在之前建立的 _context中執行;在正式執行該JS程式碼前先對JS字串進行了預處理,通過正則表示式 \\.\\s*(\\w+)\\s*\\(找到JS指令碼中的所有函式呼叫,比如demo.js中的self.getProp('data'),將函式名替換成 self.__c("getProp")('data'),即新增一層__c(arg)JS呼叫,這樣呼叫任何方法前,先呼叫__c(arg)方法,這個方法在JSPatch.js中定義,後續詳細講解這個函式的定義。

    NSString *formatedScript = [NSString stringWithFormat:@"try{%@}catch(e){_OC_catch(e.message, e.stack)}", [_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];

函式替換完成後呼叫 下面方法執行得到的JS指令碼formatedScript。

- (JSValue *)evaluateScript:(NSString *)script;

JSPatch.jsdefineClass函式可以動態新增OC類,也可以給類新增類方法、例項方法、屬性。defineClass函式定義如下,主要對傳入的例項方法和類方法呼叫_formatDefineMethods函式進行js格式化,然後再呼叫_OC_defineClass函式處理前者的結果,進行更深入的類定義、類方法以及例項方法解析:

// JSPatch.js
global.defineClass = function(declaration, instMethods, clsMethods) {
    var newInstMethods = {}, newClsMethods = {}
    _formatDefineMethods(instMethods, newInstMethods)
    _formatDefineMethods(clsMethods, newClsMethods)
    var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)
    return require(ret["cls"])
  }

其中_formatDefineMethods函式定義如下,該函式對傳入的methods字典轉化成一個新的字典,key依然是原來的方法名,value變成一個包含兩個元素的陣列,第一個元素是方法引數個數(不包括self_cmd引數),第二個元素是基於原js函式重新定義的js函式;重新定義的js函式先儲存原來的全域性self物件,然後將原js方法的第一個引數(即self)賦值給全域性的self物件,並將self引數從引數列表裡移除,利用剩下的引數呼叫原來js方法,並將執行結果返回。

// JSPatch.js
  var _formatDefineMethods = function(methods, newMethods) {
    for (var methodName in methods) {
      (function(){
       var originMethod = methods[methodName]
        newMethods[methodName] = [originMethod.length, function() {
          var args = _formatOCToJS(Array.prototype.slice.call(arguments))
          var lastSelf = global.self
          var ret;
          try {
            global.self = args[0]
            args.splice(0,1)
            ret = originMethod.apply(originMethod, args)
            global.self = lastSelf
          } catch(e) {
            _OC_catch(e.message, e.stack)
          }
          return ret
        }]
      })()
    }
  }

前面例子返回的字典為:

{"dataSource" : [0,func], "numberOfSectionsInTableView" : [1, func], "tableView_cellForRowAtIndexPath" : [2, func]}.

例項方法和類方法字典格式化後,呼叫_OC_defineClass進行進一步解析,_OC_defineClass函式其實是呼叫了OC函式static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods)
,定義如下:

   context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
        return defineClass(classDeclaration, instanceMethods, classMethods);
    };

defineClass函式做具體的解析工作, defineClass函式的具體解析步驟如下:

  • 掃描classDeclaration,提取出 classNamesuperClassName以及 protocolNames
  • 如果className類不存在,則通過執行時方法 objc_allocateClassPairobjc_registerClassPair建立並註冊className
  • 解析例項方法和類方法,
    • 將方法名中的雙下劃線__替換成單連字元-
    • 將單下劃線_替換成冒號:
    • 將單連字元-替換成單下劃線_,這裡其實就是告訴開發者如果方法名中實際包含下劃線,則需要使用雙下劃線
  • 比較替換後的方法中冒號:的個數以及_formatDefineMethods返回的方法引數個數,決定替換後的方法名後是否需要增加一個:
  • 通過呼叫 class_respondsToSelector判斷className類是否已經定義該方法,如果定義了該方法,則呼叫 overrideMethod重寫該方法,呼叫該方法不需要傳入引數描述。
  • 如果className類沒有定義該方法,呼叫 methodTypesInProtocol方法查詢繼承的protocol中有沒有定義該方法,如果定義了該方法,則返回該方法的引數描述,然後將這個引數描述作為引數呼叫overrideMethod
  • 如果繼承的protocol裡沒有定義該方法,則根據引數個數構建一個引數描述,這裡的引數個數需要加上self_cmd引數,並且所有其它引數都是id型別,然後將這個生成的引數描述作為引數呼叫overrideMethod
  • 呼叫執行時方法 class_addMethodclassName類添加了 getPropsetProp:forKey:兩個方法,這兩個方法通過關聯物件方式給類例項動態新增屬性,程式碼如下。
  • 最後返回字典 {@"cls": className}require(‘className’)JS方法,該方法將類資訊記錄在JS全域性變數global["className"] = {__isCls : 1, __clsNmae : "className"};
// defineClass函式
class_addMethod(cls, @selector(getProp:), (IMP)getPropIMP, "@@:@");
class_addMethod(cls, @selector(setProp:forKey:), (IMP)setPropIMP, "[email protected]:@@");

static id getPropIMP(id slf, SEL selector, NSString *propName) {
    return objc_getAssociatedObject(slf, propKey(propName));
}
static void setPropIMP(id slf, SEL selector, id val, NSString *propName) {
    objc_setAssociatedObject(slf, propKey(propName), val, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

OC的defineClass函式呼叫了 methodTypesInProtocol方法獲取方法的引數描述符,這個函式通過執行時方法 objc_getProtocol得到協議相關資訊,

    Protocol *protocol = objc_getProtocol([trim(protocolName) cStringUsingEncoding:NSUTF8StringEncoding]);

然後通過執行時方法 protocol_copyMethodDescriptionList獲取到協議中定義的可選(必須)的例項方法或者類方法列表,

    struct objc_method_description *methods = protocol_copyMethodDescriptionList(protocol, isRequired, isInstanceMethod, &selCount);

最後遍歷得到的列表,找到selectorName對應的objc_method_description,並將其引數描述符返回。

從上面分析可知OC方法 defineClass最終其實呼叫overrideMethod將例項方法instanceMethods和類方法classMethods新增到className類的,overrideMethod具體的實現步驟如下:

//overrideMethod函式原型
static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription)
  1. 如果傳入的引數描述符 typeDescriptionnull,則說明這個方法已經定義過,通過 class_getInstanceMethodcls類獲取到selectorName對應的Method,再通過 method_getTypeEncoding得到Method的引數描述符。
  2. 如果cls類已經實現過selectorName方法,則獲取到原來方法對應的函式實現IMP:

    IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;
    
  3. 根據typeDescription可知道方法的返回值是不是結構體,建立selectorName選擇子和 _objc_msgForward(IMP)或者 _objc_msgForward_stret(IMP)的對映關係,

    class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
  4. @selector(forwardInvocation:)的IMP設定為 JPForwardInvocation,將@selector(ORIGforwardInvocation:)的IMP設定為@selector(forwardInvocation:)原來的實現

    //overridMethod
    if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
        IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "[email protected]:@");
        class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "[email protected]:@");
    }
  5. 如果cls類原本實現過selectorName方法,則將原來的方法重新命名為ORIGselectorName,即在原來方法名加字首ORIG,

    if (class_respondsToSelector(cls, selector)) {
        NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName];
        SEL originalSelector = NSSelectorFromString(originalSelectorName);
        if(!class_respondsToSelector(cls, originalSelector)) {
            class_addMethod(cls, originalSelector, originalImp, typeDescription);
        }
    }
  6. 通過全域性字典陣列儲存cls_JPselectorNamefunction之間的對映關係:

    _initJPOverideMethods(cls);
    _JSOverideMethods[cls][JPSelectorName] = function;
  7. cls新增_JPselectorName方法,對映到前面得到的 msgForwardIMP

相關推薦

JSPatch原始碼剖析()

專案中使用到了JSPatch來實現線上APP bug的hot fix,使用後覺得JSPatch短小精悍並且功能強大,於是想往裡窺探下其實現機制。本文從JSPatch的使用角度去分析原始碼。 JavaScript執行環境建立 JavaScript執行環境的

SpringMVC原始碼剖析() DispatcherServlet

一、DispatcherServlet構造方法DispatcherServlet有兩個構造方法,一個無引數的構造方法和一個WebApplicationContext引數的構造方法,如下程式碼: public DispatcherServlet() { }

QEMU原始碼剖析()

1 qemu概述     qemu是一種快速的多體系結構模擬器,通過動態翻譯的技術達到了優異的模擬速度。目前,qemu支援兩種操作模式: 全系統模擬模式。在這種模式下,qemu完整的模擬目標平臺,此時,qemu就相當於一臺完整的pc機,例如包括一個或多個處理器以及各種外圍裝置。這種模式可以用來執行不同的

ThreadLocal終極原始碼剖析-篇足矣!

正文本文較深入的分析了ThreadLocal和InheritableThreadLocal,從4個方向去分析:原始碼註釋、原始碼剖析、功能測試、應用場景。 回到頂部一、ThreadLocal 我們使用ThreadLocal解決執行緒區域性變數統一定義問題,多執行緒資料不能共享。(InheritableThre

jdk原始碼剖析:OpenJDK-Hotspot原始碼包目錄結構

開啟正文之前,先說一下原始碼剖析這一系列,就以“死磕到底”的精神貫徹始終,JDK--》JRE--》JVM(以openJDK代替) =========正文分割線=========== 最近想看看JDK8原始碼,但JDK中JVM(安裝在本地C:\Program Files\J

NSQ原始碼剖析():NSQD主要結構方法和訊息生產消費過程

目錄 1 概述 2 主要結構體及方法 2.1 NSQD 2.2 tcpServer 2.3 protocolV2 2.4 clientV2 2.5 Topic

Flutter原始碼剖析():原始碼獲取與構建

## 概述 本文介紹了Flutter原始碼的獲取與構建,後面會另有文章介紹Flutter原始碼的版本管理、開發環境搭建等主題。 ## 準備工作 Flutter原始碼分為兩個部分: * [flutter/flutter](https://github.com/flutter/flutter)是框架層,為

STL原始碼剖析

歡迎大家來訪二笙的小房子,一同學習分享生活! 寫在前面 學習STL,瞭解STL的歷史與發展,深度剖析STL原始碼,提高自己的程式設計能力!!! 1.瞭解STL 1.1 STL概述 STL誕生:為了建立資料結構和演算法的一套標準,並且降低其間的耦合關係以提

java原始碼剖析之socket(

    不知不覺又到了新的的一週,時間在悄悄的溜走,所辛的是自己也在緩慢的推進著自己的學習計劃。      這周按照計劃檢視的是socket系列的相關類,儘管這之前就已經看過一遍,不過當時是越看越蒙,完全找不到北。 隨著自己能力的提升,回過頭來又去看一遍,還是看不懂其中的精

【原創】從原始碼剖析IO流()輸入流與輸出流--轉載請註明出處

InputStream與OutPutStream兩個抽象類,是所有的流的基礎,首先來看這兩個流的API InputStream: public abstract int read() throws IOException; 從輸入流中讀取資料的下個位元組

Spring快取原始碼剖析:()工具選擇

  從本篇開始對Spring 4.3.6版本中Cache部分做一次深度剖析。剖析過程中會對其中使用到的設計模式以及原則進行分析。相信對設計內功修煉必定大有好處。 一、環境及工具   IntelliJ IDEA 2016.2   JDK 1.8       MacOS 二、測試用程式碼   目錄整體結構是這個樣

Python 原始碼剖析)【python物件】

處於研究python記憶體釋放問題,在閱讀部分python原始碼,順便記錄下所得。 (基於《python原始碼剖析》(v2.4.1)與 python原始碼(v2.7.6)) 先列下總結:         python 中一切皆為物件,所以會先講明白pyth

darknet原始碼剖析

darknet編譯較為簡單,在github上下載程式碼後直接make即可。注意更改makefile檔案中的相關選項。 GPU=1 CUDNN=1 OPENCV=0 OPENMP=0 DEBUG=1 編譯完成後即可開始使用。根據yolov3的訓練與測試過程,對darknet

STL原始碼剖析之stl_alloc()

STL主要研究的六大問題:空間配置器,迭代器,容器,仿函式,演算法,容器介面卡 一級空間配置器原始碼如下: #if 0 #include<new> #define __THROW_BAD_ALLOC throw bad_alloc #else #include

SpringMVC原始碼剖析)- 從抽象和介面說起

註明:文章是本人在中國開源網上看到的經典文章,出處:http://my.oschina.net/lichhao 作者:相見歡 SpringMVC作為Struts2之後異軍突起的一個表現層框架,正越來越流行,相信javaee的開發者們就算沒使用過Sprin

Kaggle競賽優勝者原始碼剖析

比賽題目連結:https://www.kaggle.com/c/amazon-employee-access-challenge 優勝者Github:https://github.com/pyduan/amazonaccess     該題目提供的資料集特徵數較少,能拿到

《STL原始碼剖析》——迭代器(iterators)概念與traits程式設計技法(

一、迭代器設計思維——STL關鍵所在         STL的中心思想在於:將資料容器(containers)和演算法(algorithms)分開,彼此獨立設計,最後再以一帖粘合劑將它們撮合在一起。 二、迭代器(iterator)是一種 smart pointer    

Redis原始碼剖析和註釋(十)--- 雜湊鍵命令的實現(t_hash)

Redis 雜湊鍵命令實現(t_hash) 1. 雜湊命令介紹 Redis 所有雜湊命令如下表所示:Redis 雜湊命令詳解 序號 命令及描述 1 HDEL key field2 [field2]:刪除一個或多個雜湊表字段

Chrome原始碼剖析

開源是口好東西,它讓這個充斥著大量工業垃圾程式碼和教材玩具程式碼的行業,多了一些藝術氣息和美的潛質。它使得每個人,無論你來自米國紐約還是中國鐵嶺,都有機會站在巨人的肩膀上,如果不能,至少也可以抱一把大腿。。。現在我就是來抱大腿的,這條粗腿隸屬於Chrome(開源專案名稱其實是Chromium,本來Chrome

SpringMVC原始碼剖析)SpringMVC整體架構分析和建立

先看一下Servlet的繼承結 前面的Servlet體系我都有講過HttpServlet實現了根據動作分發請求 其他結構重要的類為HttpServletBean,FrameworkServlet ,DispatcherServlet 在Spring中實現了XXXAware