JSPatch原始碼剖析(一)
專案中使用到了JSPatch來實現線上APP bug的hot fix,使用後覺得JSPatch短小精悍並且功能強大,於是想往裡窺探下其實現機制。本文從JSPatch的使用角度去分析原始碼。
JavaScript執行環境建立
JavaScript執行環境的建立很簡單,只需要呼叫一個API即可:
[JPEngine startEngine];
+(void)startEngine
API的主要工作是建立一個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.js
JS程式碼:
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全域性上下文中定義了defineClass
、defineProtocol
等。
解析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 *)resourceURL
API將該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.js
中defineClass
函式可以動態新增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
,提取出className
,superClassName
以及protocolNames
- 如果
className
類不存在,則通過執行時方法objc_allocateClassPair
和objc_registerClassPair
建立並註冊className
類 - 解析例項方法和類方法,
- 將方法名中的雙下劃線
__
替換成單連字元-
- 將單下劃線
_
替換成冒號:
- 將單連字元
-
替換成單下劃線_
,這裡其實就是告訴開發者如果方法名中實際包含下劃線,則需要使用雙下劃線
- 將方法名中的雙下劃線
- 比較替換後的方法中冒號
:
的個數以及_formatDefineMethods
返回的方法引數個數,決定替換後的方法名後是否需要增加一個:
- 通過呼叫
class_respondsToSelector
判斷className
類是否已經定義該方法,如果定義了該方法,則呼叫overrideMethod
重寫該方法,呼叫該方法不需要傳入引數描述。 - 如果
className
類沒有定義該方法,呼叫methodTypesInProtocol
方法查詢繼承的protocol
中有沒有定義該方法,如果定義了該方法,則返回該方法的引數描述,然後將這個引數描述作為引數呼叫overrideMethod
- 如果繼承的
protocol
裡沒有定義該方法,則根據引數個數構建一個引數描述,這裡的引數個數需要加上self
和_cmd
引數,並且所有其它引數都是id
型別,然後將這個生成的引數描述作為引數呼叫overrideMethod
- 呼叫執行時方法
class_addMethod
給className
類添加了getProp
和setProp: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)
- 如果傳入的引數描述符
typeDescription
為null
,則說明這個方法已經定義過,通過class_getInstanceMethod
從cls
類獲取到selectorName
對應的Method
,再通過method_getTypeEncoding
得到Method
的引數描述符。 如果cls類已經實現過
selectorName
方法,則獲取到原來方法對應的函式實現IMP:IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;
根據
typeDescription
可知道方法的返回值是不是結構體,建立selectorName
選擇子和_objc_msgForward(IMP)
或者_objc_msgForward_stret(IMP)
的對映關係,class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
將
@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]:@"); }
如果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); } }
通過全域性字典陣列儲存
cls
、_JPselectorName
、function
之間的對映關係:_initJPOverideMethods(cls); _JSOverideMethods[cls][JPSelectorName] = function;
- 給
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