作者 | Jiang
来源 | SilverFruity,点击阅读原文查看作者更多文章
简介
OCRunner开发补丁的工作流
初衷
为了能够实现一篇文章的思路:Objective-C源码 -> 二进制补丁文件 ->热更新(具体是哪篇我忘了)。当时刚好开始了oc2mango翻译器的漫漫长路(顺带为了学习编译原理,嘻嘻),等基本完成以后,就开始肝OCRunner:完全兼容struct,enum,系统C函数调用,魔改libffi,生成补丁文件等,尽可能兼容Objective-C,为了做一个直接运行OC的快乐人。
各方职责
oc2mangoLib相当于一个简单的编译器,负责生成语法树
ORPatchFile负责将语法树序列化、反序列化和版本判断
PatchGenerator负责将oc2mangoLib和ORPatchFile的功能整合(以上工具都在oc2mango项目下)
OCRunner负责解释执行语法树
与其他库的区别
下发二进制补丁文件。增加安全性,减小补丁大小,省去词法分析与语法分析,优化启动时间,可在PatchGenerator阶段进行优化
自定义的Arm64 ABI (可以不使用libffi)
完整的Objective-C语法支持,除去预编译和部分语法
本地使用OCRunner运行补丁
OCRunnerDemo https://github.com/SilverFruity/OCRunner/tree/master/OCRunnerDemo 可以作为整个流程的参照.
Cocoapods导入OCRunner
pod 'OCRunner' #支持所有架构,包含libffi.a
# 或者
pod 'OCRunnerArm64' #仅支持 arm64和arm64e,没有libffi.a
下载 PatchGenerator
解压PatchGenerato.zip https://github.com/SilverFruity/oc2mango/releases,然后将PatchGenerator保存到/usr/bin/或项目目录下.
添加PatchGenerator的 Run Script
Project Setting -> Build Phases -> 左上角的 + -> New Run Script Phase
PatchGenerator的路径 -files Objective-C源文件列表或者文件夹 -refs Objective-C头文件列表或者文件夹 -output 输出补丁保存的位置
比如OCRunnerDemo中的Run Script
$SRCROOT/OCRunnerDemo/PatchGenerator -files $SRCROOT/OCRunnerDemo/ViewController1 -refs $SRCROOT/OCRunnerDemo/Scripts.bundle -output $SRCROOT/OCRunnerDemo/binarypatch
开发环境下: 运行补丁
将生成的补丁文件作为资源文件添加到项目中
// Appdelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
#if DEBUG
NSString *patchFilePath = [[NSBundle mainBundle] pathForResource:@"PatchFileName" ofType:nil];
#else
// download from server
#endif
[ORInterpreter excuteBinaryPatchFile:patchFilePath];
return YES;
}
每次修改文件,记得Command+B,调用Run Scrip,重新生成补丁文件.
正式环境
将补丁上传到服务器
App中下载补丁文件并保存到本地
使用[ORInterpreter excuteBinaryPatchFile:PatchFilePath] 执行补丁
使用介绍
引入结构体、枚举、typedef
可以通过修改OCRunnerDemo中的ViewController1,运行以下代码.
// 将添加一个名为dispatch_once_t的新类型
typedef NSInteger dispatch_once_t;
// link NSLog
void NSLog(NSString *format, ...);
typedef enum: NSUInteger{
UIControlEventTouchDown = 1 << 0,
UIControlEventTouchDownRepeat = 1 << 1,
UIControlEventTouchDragInside = 1 << 2,
UIControlEventTouchDragOutside = 1 << 3,
UIControlEventTouchDragEnter = 1 << 4
}UIControlEvents;
int main(){
UIControlEvents events = UIControlEventTouchDown | UIControlEventTouchDownRepeat;
if (events & UIControlEventTouchDown){
NSLog(@"UIControlEventTouchDown");
}
NSLog(@"enum test: %lu",events);
return events;
}
main();
Tips:
推荐新建一个文件来放置以上代码,类似于OCRunnerDemo中的UIKitRefrence和GCDRefrence文件,然后使用PatchGenerator以-links的形式加入补丁生成。作者想偷偷懒,不想再去CV了,头文件太多了?.
使用系统内置C函数
//you only need to add the C function declaration in Script.
//link NSLog
void NSLog(NSString *format, ...);
//then you can use it in Scrtips.
NSLog(@"test for link function %@", @"xixi");
当你运行以上代码时. OCRunner将会使用ORSearchedFunction 搜索函数的指针.
这个过程的核心实现是 SymbolSearch (修改自fishhook).
如果搜索到的结果是NULL,OCRunner将会自动在控制台打印如下信息:
|----------------------------------------------|
|❕you need add ⬇️ code in the application file |
|----------------------------------------------|
[ORSystemFunctionTable reg:@"dispatch_source_set_timer" pointer:&dispatch_source_set_timer];
修复OC对象(类)方法、添加属性
想修复哪个方法,将改方法实现即可,不用实现其他方法.
@interface ORTestClassProperty:NSObject
@property (nonatomic,copy)NSString *strTypeProperty;
@property (nonatomic,weak)id weakObjectProperty;
@end
@implementation ORTestClassProperty
- (void)otherMethod{
self.strTypeProperty = @"Mango";
}
- (NSString *)testObjectPropertyTest{
[self ORGtestObjectPropertyTest] //方法名前加'ORG'调用原方法
[self otherMethod];
return self.strTypeProperty;
}
@end
Block使用、解决循环引用
// 用于解决循环引用
__weak id object = [NSObject new];
// 最简block声明
void (^a)(void) = ^{
int b = 0;
};
a();
使用GCD
本质就是 使用系统内置C函数,通过GCDRefrences文件添加,GCD相关的函数声明以及typedef皆在其中.
比如:
// link dispatch_sync
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
void main(){
dispatch_queue_t queue = dispatch_queue_create("com.plliang19.mango",DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
completion(@"success");
});
}
main();
使用内联函数、预编译函数
// 内联函数:在补丁中,添加一个全局函数中即可,比如UIKitRefrences中的CGRectMake
CGRect CGRectMake(CGFloat x, CGFloat y, CGFloat width, CGFloat height)
{
CGRect rect;
rect.origin.x = x; rect.origin.y = y;
rect.size.width = width; rect.size.height = height;
return rect;
}
// 预编译函数:需要在App中预埋
[[MFScopeChain top] setValue:[MFValue valueWithBlock:^void(dispatch_once_t *onceTokenPtr,
dispatch_block_t _Nullable handler){
dispatch_once(onceTokenPtr,handler);
}] withIndentifier:@"dispatch_once"];
如何确定补丁中是否包含源文件
查看Run Script打印的 InputFiles 中是否包含源文件.
性能测试
根据已知数据,OCRunner的补丁加载速度是JSPatch的20倍+,随着补丁大小的不断增加,这个倍数会不断增加。运行速度和内存占用与MangoFix差距不大。内存占用方面应该会更优,OCRunner中MFValue的值采用malloc来复制值,不会有多个类型的实例变量。
目前的问题
指针与乘号识别冲突问题,衍生的问题:类型转换等等
不支持static、inline函数声明
不支持C数组声明: type a[]和type a[2],以及 value = { 0 , 0 , 0 , 0 } 这种表达式
不支持 ‘->’ 操作符号
不支持C函数替换
支持语法
类声明与实现,支持分类写法
Protocol
Block语法
结构体、枚举、typedef
使用函数声明,链接系统函数指针
全局函数
多参数调用(方法和函数)
*、& (指针操作)
变量static关键字
NSArray: @[value1, value2],NSDictionary: @{ key: value }, NSNumer: @(value)
NSArray取值和NSDictionary取值和赋值语法,id value = a[var]; a[var] = value;
运算符,除去’->’皆已实现
… 等
想到了再加吧?
目标
完善当前的语法支持
更多的单元测试覆盖(尽管目前显示是84%)
PatchGenerator阶段进行优化:未被调用的函数声明、结构体、枚举等,不会在补丁中,减少包大小以及加载时间等
尝试Swift热更新(新建库吧,哈哈)