我们透过系统底层来捕获ui事件流和业务数据的流动,并利用捕获到的这些数据通过事件回放机制来复现线上的问题。本文先介绍录制和回放的整体框架,接着介绍里面涉及到的3个关键技术点,也是这里最复杂的技术(模拟触摸事件,统一拦截器实现,统一hook block)。

背景

现在的app基本都会提供用户反馈问题的入口,然而提供给用户反馈问题一般有两种方式:

●  直接用文字输入表达,或者截图
 ●  直接录制视频反馈

这两种反馈方式常常带来以下抱怨:

●  用户:输入文字好费时费力
 ●  开发1:看不懂用户反馈说的是什么意思?
 ●  开发2:大概看懂用户说的是什么意思了,但是我线下没办法复现哈
 ●  开发3:看了用户录制的视频,但是我线下没办法重现,也定位不到问题

所以,为了解决以上问题,我们用一套全新的思路来设计线上问题回放体系。

线上问题回放体系的意义

●  用户不需要输入文字反馈问题,只需要重新操作一下app重现问题步骤即可。
 ●  开发者拿到用户反馈的问题脚本后,通过线下回放对问题一目了然,跟录制视频效果一样,是的,你没看错,就是跟看视频一样。
 ●  通过脚本的回放实时获取到app运行时相关数据(本地数据,网络数据,堆栈等等), 以便排查问题。
 ●  为后续自动测试提供想象空间——你懂的。

效果视频

技术原理

1.app与外部环境的关系

从上面的关系图可以看出,整个app的运行无非是用户ui操作,然后触发app从外界获取数据,包括网络数据,gps数据等等,也包括从手机本地获取数据,比如相册数据,机器数据,系统等数据。 所以我们要实现问题回放只需要记录用户的UI操作和外界数据,app自身数据即可。

app录制 = 用户的UI操作 + 外界数据(手机内和手机外) + app自身数据

2.线上问题回放架构由两部分组成:录制和回放

录制是为回放服务,录制的信息越详细,回放成功率就越高,定位问题就越容易

录制其实就是把ui和数据记录下来,回放其实就是app自动驱动UI操作并把录制时的数据塞回相应的地方。

3.录制架构图

录制流程:

4.回放架构图

回放跟录制框架图基本一样,实际上录制和回放的代码是在一起,逻辑也是统一的,为了便于表达,我人为划分成两个架构图出来。

回放的流程:

1.启动app,点击回放按钮。

2.引擎加载回放脚本。

3.从脚本中解析出需要注册的运行时事件并注册,在回放里不需要业务上层来注册事件,这里跟录制是不一样的。

4.从脚本中解析出需要注册的静态数据事件并注册。

5.从脚本中解析出需要播放的事件数据,并组成消费队列。

6.启动播放器,从消费队列里读取一个个事件来播放,如果是ui事件则直接播放,如果是静态数据事件则直接按照指令要求替换数据值,如果是非ui运行时事件则通过事件指令规则来确定是主动播放还是等待拦截对应的事件,如果需要等待拦截对应的事件,则播放器会一直等待此事件直到此事件被app消费掉为止。只有此事件被消费了,播放器才能播放下一个事件。

7.当拦截到被注册的事件后,根据此事件指令要求把相应的数据塞到相应的字段里。

8.跳回6继续运行,直到消费队列里的事件被消费完。

注意:回放每个事件时会实时自动打印出相应的堆栈信息和事件数据,有利于排查问题

关键技术介绍

1.模拟触摸事件

从ui事件数据解中析出被触摸的view,以及此view所在的视图树中的层级关系,并在当前回放界面上查找到对应的view,然后往该view上发送ui操作事件(点击,双击等等),并带上触摸事件的坐标信息,其实这里是模拟触摸事件。我们先来介绍触摸事件的处理流程:

等待触摸阶段

●  手机屏幕处于待机状态,等待触摸事件发生
 ●  手指开始触摸屏幕

系统反应阶段

●  屏幕感应器接收到触摸,并将触摸数据传给系统IOKit(IOKit是苹果的硬件驱动框架)
 ●  系统IOKit封装该触摸事件为IOHIDEvent对象
 ●  接着系统IOKit把IOHIDEvent对象转发给SpringBoard进程

SpringBoard进程就是iOS的系统桌面,它存在于iDevice的进程中,不可清除,它的运行原理与Windows中的explorer.exe系统进程相类似。它主要负责界面管理,所以只有它才知道当前触摸到底有谁来响应。

SpringBoard接收阶段

●  SpringBoard收到IOHIDEvent消息后,触发runloop中的Source1回调__IOHIDEventSystemClientQueueCallback()方法。
 ●  SpringBoard开始查询前台是否存在正在运行的app,如果存在,则SpringBoard通过进程通信方式把此触摸事件转发给前台当前app,如果不存在,则SpringBoard进入其自己内部响应过程。

app处理阶段

●  前台app主线程Runloop收到SpringBoard转发来的消息,并触发对应runloop 中的Source1回调_UIApplicationHandleEventQueue()。
 ●  _UIApplicationHandleEventQueue()把IOHIDEvent处理包装成UIEvent进行处理分发。
 ●  Soucre0回调内部UIApplication的sendEvent:方法,将UIEvent传给UIWindow。
 ●  在UIWindow为根节点的整棵视图树上通过hitTest(_:with:)和point(inside:with:)这两个方法递归查找到合适响应这个触摸事件的视图。
 ●  找到最终的叶子节点视图后,就开始触发此视图绑定的相应事件,比如跳转页面等等。

从上面触摸事件处理过程中我们可以看出要录制ui事件只需要在app处理阶段中的UIApplication sendEvent方法处截获触摸数据,回放时也是在这里把触摸模拟回去。

下面是触摸事件录制的代码,就是把UITouch相应的数据保存下来即可 这里有一个关键点,需要把touch.timestamp的时间戳记录下来,以及把当前touch事件距离上一个touch事件的时间间隔记录下来,因为这个涉及到触摸引起惯性加速度问题。比如我们平时滑动列表视图时,手指离开屏幕后,列表视图还要惯性地滑动一小段时间。


- (void)handleUIEvent:(UIEvent *)event{if (!self.isEnabled) return;if (event.type != UIEventTypeTouches) return;NSSet *allTouches = [event allTouches];UITouch *touch = (UITouch *)[allTouches anyObject];if (touch.view) {if (self.filter && !self.filter(touch.view)) {return;}}switch (touch.phase) {case UITouchPhaseBegan:{self.machAbsoluteTime = mach_absolute_time();self.systemStartUptime = touch.timestamp;self.tuochArray = [NSMutableArray array];[self recordTouch:touch click:self.machAbsoluteTime];break;}case UITouchPhaseStationary:{[self recordTouch:touch click:mach_absolute_time()];break;}case UITouchPhaseCancelled:{[self recordTouch:touch click:mach_absolute_time()];[[NSNotificationCenter defaultCenter] postNotificationName:@"notice_ui_test" object:self.tuochArray];break;}case UITouchPhaseEnded:{[self recordTouch:touch click:mach_absolute_time()];[[NSNotificationCenter defaultCenter] postNotificationName:@"notice_ui_test" object:self.tuochArray];break;}case UITouchPhaseMoved:{[self recordTouch:touch click:mach_absolute_time()];}default:break;}}

我们来看一下代码怎么模拟单击触摸事件(为了容易理解,我把有些不是关键,复杂的代码已经去掉),接着我们来看一下模拟触摸事件代码 一个基本的触摸事件一般由三部分组成:

●  UITouch对象 - 将用于触摸
 ●  第一个UIEvent Began触摸
 ●  第二个UIEvent Ended触摸

实现步骤:

1.代码的前面部分都是一些UITouch和UIEvent私有接口,私有变量字段,由于苹果并不公开它们,为了让其编译不报错,所以我们需要把这些字段包含进来,回放是在线下,所以不必担心私有接口被拒的事情。

2.构造触摸对象:UITouch和UIEvent,把记录对应的字段值塞回相应的字段。塞回去就是用私有接口和私有字段。

3.触摸的view位置转换为Window坐标,然后往app里发送事件 [[UIApplication sharedApplication] sendEvent:event];

4.要回放这些触摸事件,我们需要把他丢到CADisplayLink里面来执行。

//// SimulationTouch.m//// Created by 诗壮殷 on 2018/5/15.//#import "SimulationTouch.h"#import <objc/runtime.h>#include <mach/mach_time.h>@implementation UITouch (replay)- (id)initPoint:(CGPoint)point window:(UIWindow *)window{NSParameterAssert(window);self = [super init];if (self) {[self setTapCount:1];[self setIsTap:YES];[self setPhase:UITouchPhaseBegan];[self setWindow:window];[self _setLocationInWindow:point resetPrevious:YES];[self setView:[window hitTest:point withEvent:nil]];[self _setIsFirstTouchForView:YES];[self setTimestamp:[[NSProcessInfo processInfo] systemUptime]];}return self;}@end@interface UIInternalEvent : UIEvent- (void)_setHIDEvent:(IOHIDEventRef)event;@end@interface UITouchesEvent : UIInternalEvent- (void)_addTouch:(UITouch *)touch forDelayedDelivery:(BOOL)delayedDelivery;- (void)_clearTouches;@endtypedef enum {kIOHIDDigitizerEventRange = 0x00000001,kIOHIDDigitizerEventTouch = 0x00000002,kIOHIDDigitizerEventPosition = 0x00000004,} IOHIDDigitizerEventMask;IOHIDEventRef IOHIDEventCreateDigitizerFingerEvent(CFAllocatorRef allocator,AbsoluteTime timeStamp,uint32_t index,uint32_t identity,IOHIDDigitizerEventMask eventMask,IOHIDFloat x,IOHIDFloat y,IOHIDFloat z,IOHIDFloat tipPressure,IOHIDFloat twist,Boolean range,Boolean touch,IOOptionBits options);@implementation SimulationTouch- (void)performTouchInView:(UIView *)view start:(bool)start{UIWindow *_window = view.window;CGRect fInWindow;if ([view isKindOfClass:[UIWindow class]]){fInWindow = view.frame;}else{fInWindow = [_window convertRect:view.frame fromView:view.superview];}CGPoint point =CGPointMake(fInWindow.origin.x + fInWindow.size.width/2,fInWindow.origin.y + fInWindow.size.height/2);if(start){self.touch = [[UITouch alloc] initPoint:point window:_window];[self.touch setPhase:UITouchPhaseBegan];}else{[self.touch _setLocationInWindow:point resetPrevious:NO];[self.touch setPhase:UITouchPhaseEnded];}CGPoint currentTouchLocation = point;UITouchesEvent *event = [[UIApplication sharedApplication] _touchesEvent];[event _clearTouches];uint64_t machAbsoluteTime = mach_absolute_time();AbsoluteTime timeStamp;timeStamp.hi = (UInt32)(machAbsoluteTime >> 32);timeStamp.lo = (UInt32)(machAbsoluteTime);[self.touch setTimestamp:[[NSProcessInfo processInfo] systemUptime]];IOHIDDigitizerEventMask eventMask = (self.touch.phase == UITouchPhaseMoved)? kIOHIDDigitizerEventPosition: (kIOHIDDigitizerEventRange | kIOHIDDigitizerEventTouch);Boolean isRangeAndTouch = (self.touch.phase != UITouchPhaseEnded);IOHIDEventRef hidEvent = IOHIDEventCreateDigitizerFingerEvent(kCFAllocatorDefault,timeStamp,0,2,eventMask,currentTouchLocation.x,currentTouchLocation.y,0,0,0,isRangeAndTouch,isRangeAndTouch,0);if ([self.touch respondsToSelector:@selector(_setHidEvent:)]) {[self.touch _setHidEvent:hidEvent];}[event _setHIDEvent:hidEvent];[event _addTouch:self.touch forDelayedDelivery:NO];[[UIApplication sharedApplication] sendEvent:event];}@end

总的来说就下载苹果提供触摸事件的源码库,分析源码,然后设置断掉调试,甚至反汇编来理解触摸事件的原理。

2.统一拦截器

录制和回放都居于事件流来处理的,而数据的事件流其实就是对一些关键方法的hook,由于我们为了保证对业务代码无侵入和扩展性(随便注册事件),我们需要对所有方法统一hook,所有的方法由同一个钩子来响应处理。如下图所示

这个钩子是用用汇编编写,由于汇编代码比较多,而且比较难读懂,所以这里暂时不附上源码,汇编层主要把硬件里面的一些数据统一读取出来,比如通用寄存器数据和浮点寄存器数据,堆栈信息等等,甚至前面的前面的方法参数都可以读取出来,最后转发给c语言层处理。

汇编层把硬件相关信息组装好后调用c层统一拦截接口,汇编层是为c层服务。c层无法读取硬件相关信息,所以这里只能用汇编来读取。c层接口通过硬件相关信息定位到当前的方法是属于哪个事件,知道了事件,也意味着知道了事件指令,知道了事件指令,也知道了哪些字段需要塞回去,也知道了被hook的原始方法。

c层代码介绍如下: 由于是统一调用这个拦截器,所以拦截器并不知道当前是哪个业务代码执行过来的,也不知道当前这个业务方法有多少个参数,每个参数类型是什么等等,这个接口代码处理过程大概如下:

●  通过寄存器获取对象self
 ●  通过寄存器获取方法sel
 ●  通过self和sel获取对应的事件指令
 ●  通过事件指令回调上层来决定是否往下执行
 ●  获取需要回放该事件的数据
 ●  把数据塞回去,比如塞到某个寄存器里,或者塞到某个寄存器所指向的对象的某个字段等等
 ●  如果需要立即回放则调用原来被hook的原始方法,如果不是立即回放,则需要把现场信息保存起来,并等待合适的时机由播放队列来播放(调用)

//xRegs 表示统一汇编器传入当前所有的通用寄存器数据,它们地址存在一个数组指针里//dRegs 表示统一汇编器传入当前所有的浮点寄存器数据,它们地址也存在一个数组指针里//dRegs 表示统一汇编器传入当前堆栈指针//fp 表示调用栈帧指针void replay_entry_start(void* xRegs, void* dRegs, void* spReg, CallBackRetIns *retIns,StackFrame *fp, void *con_stub_lp){void *objAdr = (((void **)xRegs)[0]);//获取对象本身self或者block对象本身EngineManager *manager = [EngineManager sharedInstance];ReplayEventIns *node = [manager getEventInsWithBlock:objAdr];id obj = (__bridge id)objAdr;void *xrArg = ((void **)xRegs)+2;if(nil == node){SEL selecter = (SEL)(((void **)xRegs)[1]); //对应的对象调用的方法Class tclass = [obj class];//object_getClass(obj);object_getClass方法只能通过对象获取它的类,不能传入class 返回class本身,do{node = [manager getEventIns:tclass sel:selecter];//通过对象和方法获取对应的事件指令节点}while(nil == node && (tclass = class_getSuperclass(tclass)));}else{xrArg = ((void **)xRegs)+1;}assert(node && "node is nil in replay_call_start");//回调通知上层当前回放是否打断if(node.BreakCurReplayExe && node.BreakCurReplayExe(obj,node,xrArg,dRegs)){retIns->nodeAddr = NULL;retIns->recordOrReplayData = NULL;retIns->return_address = NULL;return;}bool needReplay = true;//回调通知上层当前即将回放该事件if(node.willReplay){needReplay = (*(node.willReplay))(obj,node,xrArg,dRegs);}if(needReplay){ReplayEventData *replayData = nil;if(node.getReplayData){//获取回放该事件对应的数据replayData = (*(node.getReplayData))(obj,node,xrArg,dRegs);}else//默认获取方法{replayData = [manager getNextReplayEventData:node];}//以下就是真正的回放,即是把数据塞回去,并调用原来被hook的方法if(replayData){if(replay_type_intercept_call == node.replayType){sstuffArg(xRegs,dRegs,spReg,node,replayData.orgDic);NSArray *arglist = fetchAllArgInReplay(xRegs, dRegs, spReg, node);ReplayInvocation *funobj = [[ReplayInvocation alloc] initWithFunPtr:node.callBack ? node.callBack : [node getOrgFun]args:arglistargType:[node getFunTypeStr]retType:rf_return_type_v];if([[EngineManager sharedInstance] setRepalyEventReady:replayData funObj:funobj]){//放到播放队列里播放,返回没调用地址,让其不往下走retIns->return_address = NULL;return ;}}else{//塞数据sstuffArg(xRegs,dRegs,spReg,node,replayData.orgDic);}}retIns->nodeAddr = (__bridge void *)node;retIns->recordOrReplayData = (__bridge void *)replayData;retIns->return_address = node.callBack ? node.callBack : [node getOrgFun];replayData.runStatus = relay_event_run_status_runFinish;}else{retIns->nodeAddr = NULL;retIns->recordOrReplayData = NULL;retIns->return_address = [node getOrgFun];}}

3.怎样统一hook block

如果你只是想大概理解block的底层技术,你只需google一下即可。 如果你想全面深入的理解block底层技术,那网上的那些资料远远满足不了你的需求。 只能阅读苹果编译器clang源码和列出比较有代表性的block例子源码,然后转成c语言和汇编,通过c语言结合汇编研究底层细节。

何谓 oc block?

●  block就是闭包,跟回调函数callback很类似,闭包也是对象。
 ●  blcok的特点: 1.可有参数列表 2.可有返回值 3.有方法体 4.capture上下文变量 5.有对象引用计数的内存管理策略(block生命周期)。
 ●  block的一般存储在内存中形态有三种 _NSConcretStackBlock(栈)_NSConcretGlobalBlock(全局)_NSConcretMallocBlock(堆)。

系统底层怎样表达block?

我们先来看一下block的例子:

void test(){__block int var1 = 8; //上下文变量NSString *var2 = @"我是第二个变量”; //上下文变量void (^block)(int) = ^(int arg)//参数列表{var1 = 6;NSLog(@"arg = %d,var1 = %d, var2 = %@", arg, var1, var2);};block(1);//调用block语法dispatch_async(dispatch_get_global_queue(0, 0), ^{block(2); //异步调用block});}

这段代码首先定义两个变量,接着定义一个block,最后调用block。

●  两个变量:这两个变量都是被block引用,第一个变量有关键字__block,表示可以在block里对该变量赋值,第二个变量没有__block关键字,在block里只能读,不能写。
 ●  两个调用block的语句:第一个直接在当前方法test()里调用,此时的block内存数据在栈上,第二个是异步调用,就是说当执行block(2)时test()可能已经运行完了,test()调用栈可能已经被销毁。那这种情况block的数据肯定不能在栈上,只能在堆上或者在全局区。

系统底层表达block比较重要的几种数据结构如下:

注意:虽然底层是用这些结构体来表达block,但是它们并不是源码,是二进制代码

enum{BLOCK_REFCOUNT_MASK = (0xffff),BLOCK_NEEDS_FREE = (1 << 24),BLOCK_HAS_COPY_DISPOSE = (1 << 25),BLOCK_HAS_CTOR = (1 << 26),//todo == BLOCK_HAS_CXX_OBJ?BLOCK_IS_GC = (1 << 27),BLOCK_IS_GLOBAL = (1 << 28),BLOCK_HAS_DESCRIPTOR = (1 << 29),//todo == BLOCK_USE_STRET?BLOCK_HAS_SIGNATURE = (1 << 30),OBLOCK_HAS_EXTENDED_LAYOUT = (1 << 31)};enum{BLOCK_FIELD_IS_OBJECT = 3,BLOCK_FIELD_IS_BLOCK = 7,BLOCK_FIELD_IS_BYREF = 8,OBLOCK_FIELD_IS_WEAK = 16,OBLOCK_BYREF_CALLER = 128};typedef struct block_descriptor_head{unsigned long int reserved;unsigned long int size; //表示主体block结构体的内存大小}block_descriptor_head;typedef struct block_descriptor_has_help{unsigned long int reserved;unsigned long int size; //表示主体block结构体的内存大小void (*copy)(void *dst, void *src);//当block被retain时会执行此函数指针void (*dispose)(void *);//block被销毁时调用struct block_arg_var_descriptor *argVar;}block_descriptor_has_help;typedef struct block_descriptor_has_sig{unsigned long int reserved;unsigned long int size;const char *signature;//block的签名信息struct block_arg_var_descriptor *argVar;}block_descriptor_has_sig;typedef struct block_descriptor_has_all{unsigned long int reserved;unsigned long int size;void (*copy)(void *dst, void *src);void (*dispose)(void *);const char *signature;struct block_arg_var_descriptor *argVar;}block_descriptor_has_all;typedef struct block_info_1{void *isa;//表示当前blcok是在堆上还是在栈上,或在全局区_NSConcreteGlobalBlockint flags; //对应上面的enum值,这些枚举值是我从编译器源码拷贝过来的int reserved;void (*invoke)(void *, ...);//block对应的方法体(执行体,就是代码段)void *descriptor;//此处指向上面几个结构体中的一个,具体哪一个根据flags值来定,它用来进一步来描述block信息//从这个字段开始起,后面的字段表示的都是此block对外引用的变量。NSString *var2;byref_var1_1 var1;} block_info_1;

这个例子中的block在底层表达大概如下图:

首先用block_info_1来表达block本身,然后用block_desc_1来具体描述block相关信息(比如block_info_1结构体大小,在堆上还是在栈上?copy或dispose时调用哪个方法等等),然而block_desc_1具体是哪个结构体是由block_info_1中flags字段来决定的,block_info_1里的invoke字段是指向block方法体,即是代码段。block的调用就是执行这个函数指针。由于var1是可写的,所以需要设计一个结构体(byref_var1_1)来表达var1,为什么var2直接用他原有的类型表达,而var1要用结构体来表达。篇幅有限,这个自己想想吧?

block小结

●  为了表达block,底层设计三种结构体:block_info_1,block_desc_1,byref_var1_1,三种函数指针: block invoke方法体,copy方法,dispose方法
 ●  其实表达block是非常复杂的,还涉及到block的生命周期,内存管理问题等等,我在这里只是简单的贯穿主流程来介绍的,很多细节都没介绍。

怎样统一 hook block?

通过上面的分析,得知oc里的block就是一个结构体指针,所以我在源码里可以直接把它转成结构体指针来处理。 统一hook block源码如下:

VoidfunBlock createNewBlock(VoidfunBlock orgblock, ReplayEventIns *blockEvent,bool isRecord){if(orgblock && blockEvent){VoidfunBlock newBlock = ^(void){orgblock();if(nil == blockEvent){assert(0);}};trace_block_layout *blockLayout = (__bridge trace_block_layout *)newBlock;blockLayout->invoke = (void (*)(void *, ...))(isRecord?hook_var_block_callBack_record:hook_var_block_callBack_replay);return newBlock;}return nil;}

我们首先新建一个新的block newBlock,然后把原来的block orgblock 和 事件指令blockEvent包到新的blcok中,这样达到引用的效果。然后把

新的block转

成结构体指针,并把结构体指针中的字段invoke(方法体)指向统一回调方法。你可能诧异新的block是没有参数类型的,原来block是有参数类型,

外面调用原

来block传递参数时会不会引起crash?答案是否定的,因为这里构造新的block时 我们只用block数据结构,block的回调方法字段已经被阉割,回

调方法已经指

向统一方法了,这个统一方法可以接受任何类型的参数,包括没有参数类型。这个统一方法也是汇编实现,代码实现跟上面的汇编层代码类似,这

里就不附上源

码了。

那怎样在新的blcok里读取原来的block和事件指令对象呢? 代码如下:

void var_block_callback_start_record(trace_block_layout * blockLayout){VoidfunBlock orgBlock = (__bridge VoidfunBlock)(*((void **)((char *)blockLayout + sizeof(trace_block_layout))));ReplayEventIns *node = (__bridge ReplayEventIns *)(*((void **)((char *)blockLayout + 40)));}

总结

本文大概介绍了问题回放框架,接着介绍三个关键技术。这三个技术相对比较深入,欢迎在留言区评论,我们期待与大家交流,共同探讨。

阿里云双十一1折拼团活动:满6人,就是最低折扣了!
【满6人】1核2G云服务器99.5元一年298.5元三年 2核4G云服务器545元一年 1227元三年
【满6人】1核1G MySQL数据库 119.5元一年
【满6人】3000条国内短信包 60元每6月
参团地址:http://click.aliyun.com/m/1000020293/

原文链接
本文为云栖社区原创内容,未经允许不得转载。

工程师如何“神还原”用户问题?闲鱼回放技术揭秘相关推荐

  1. 【干货】阿里资深无线技术专家孙兵谈闲鱼社区技术架构演进

    近期在ArchSummit北京会议上,阿里巴巴资深无线技术专家孙兵(花名酒丐)发表了<网格社区-闲鱼技术架构演讲>主题演讲.孙兵2011年加入阿里巴巴,先后在B2B.淘宝.手机淘宝等部门负 ...

  2. 重磅发布 | 承载亿级流量的开发框架,闲鱼Flutter技术解析与实战大公开

    简介: 闲鱼是国内最早接触使用 Flutter 的团队,经过多次研讨验证并大规模上线,在App性能.稳定性.开发效率上收益甚多.现在,闲鱼将这个过程中的一手实践知识和技术沉淀,整理成册 --<F ...

  3. 闲鱼前端技术体系的背后——魔鱼(良心推荐,从思路到实践)

    闲鱼经过近八年的发展,前端技术在整个研发体系中有着举足轻重的地位,前端有迭代速度快,动态化能力强,跨端适配成本低等显著优势.闲鱼前端一直使用淘系提供的基础技术和平台工具,但随着业务的不断发展,逐渐无法 ...

  4. 为了帮助卖家成交,闲鱼工程师做了些什么?

    引言 闲鱼是一个C2C平台,提高卖家活跃度不仅有利于成交的提升,对于用户增长也有积极意义.而其中的关键点就在于其成交的效率.而个人卖家由于其专业程度不如专业卖家,成交效率往往并不高.我们希望可以实现两 ...

  5. 闲鱼冻结多个欺诈用户:还是治标不治本

    5月24日,近日闲鱼又冻结了1.2万个诈骗用户,部分用户被永久冻结.这两年闲鱼官方一直在封杀欺诈用户,然而效果似乎并不理想.作为二手交易平台,闲鱼.转转等已经逐渐失去了口碑,因平台上的骗子太多而被广大 ...

  6. 一个闲鱼挂机项目,让淘宝用户彻底“躺赢”

    闲鱼挂机项目,单号日收益约3元,这个项目比较稳定,并且目前做的人比较少,值得操作,每天仅需2分钟,可无限放大 一.项目介绍 闲鱼币可以用来推广闲鱼的商品,所以很多人需要闲鱼币,也由此产生了买卖:我们通 ...

  7. 闲鱼靠什么支撑起万亿的交易规模?| 云原生Talk

    造梦者 | 王树彬,阿里巴巴闲鱼架构负责人 2014年6月28日,阿里即将赴美上市的这一年,西溪园区的一个茶水间里,28个人日夜赶工了三个月后,上线了一个闲置交易平台--闲鱼.今年5月份,在阿里巴巴的 ...

  8. GMTC2019|闲鱼-基于Flutter的架构演进与创新

    2012年应届毕业加入阿里巴巴,主导了闲鱼基于Flutter的新混合架构,同时推进了Flutter在闲鱼各业务线的落地.未来将持续关注终端技术的演变及趋势 Flutter的优势与挑战 Flutter是 ...

  9. 闲鱼的云原生故事:靠什么支撑起万亿的交易规模?

    来源 | 阿里巴巴中间件 作者 | 王树彬,阿里巴巴闲鱼架构负责人 责编 | Carol 2014年6月28日,阿里即将赴美上市的这一年,西溪园区的一个茶水间里,28个人日夜赶工了三个月后,上线了一个 ...

最新文章

  1. [笔记]C#基础入门(八)——C#标识符的命名规则
  2. python代码 程序员编程艺术 2.1
  3. 前端学习(563):干掉block重叠margin重叠
  4. MyEclipse移动开发教程:迁移HTML5移动项目到PhoneGap(二)
  5. 初学Linux第三周
  6. CVPR 2020百度-涵盖全视觉领域22篇
  7. qml 信号槽第二次才响应_QML中各种代理的用法
  8. python 对角阵_numpy创建单位矩阵和对角矩阵的实例
  9. Tensorflow:操作执行原理
  10. 龙星电脑横机制版软件_龙星制版软件下载 龙星电脑横机是什么系统
  11. mysql中部门表和员工表_数据库 员工表和部门表
  12. 杰里之 2M 的 SDK 开蓝牙一拖二出现奇怪的问题【篇】
  13. 什么是SEO?SEO的区别在哪里?
  14. python死循环_python中死循环
  15. postman设置成中文
  16. flex: 1到底是什么意思?
  17. Javascript中大于和小于
  18. 0X000000该内存不能为read的解决方法(转)
  19. Mac电脑如何调整鼠标光标大小?
  20. 深入理解GO语言:GC原理及源码分析

热门文章

  1. 【LeetCode笔记】剑指 Offer 14. 剪绳子 I II(Java、动态规划、偏数学)
  2. 【LeetCode笔记】560. 和为K的子数组(Java、前缀和、哈希表)
  3. 虚拟机linux如何扩大内存吗,如何扩大Vmware虚拟机中Ubuntu系统磁盘空间的方法
  4. rac一节点时间比另一个节点快_数据库数据那么多为什么可以检索这么快?
  5. arraylist线程安全吗_Java的线程安全、单例模式、JVM内存结构等知识梳理
  6. android lottie字体json,Android 动画深入Lottie
  7. 循环自增_大学C语言—循环结构及应用
  8. 诸多研究生的一个通病:对导师过度依赖!
  9. 温柔又有耐心的男孩最吸引人
  10. “杨振宁理论物理研究所”