Sunglasses,太阳镜,希望能让你在如火如荼的移动端开发过程中变的更 cool !
Sunglasses是京东主站的购物车团队孵化出的用于在开发、测试阶段使用的提效工具集,解决了由于组件化所导致的各团队提效工具分散、共享难度大的痛点,目前已经融合了京东主站各开发团队贡献的包括代码调试、UI调试、性能调优、数据调试等多种类型的提效工具,拥有接入成本低、代码无侵入、使用简便等优点,以不参与上线的组件化模块形式帮助各开发团队提升效率,目前已经参与到京东商城、京喜、京东极速版等App的版本迭代过程中。未来Sunglasses将持续优化共建方式,完善工具覆盖场景,打磨工具使用体验,并扩大支持App的范围,帮助各团队持续提效,成为研发、测试环节中更 cool 的一环!
痛点分析:测试阶段测试工程师触发崩溃后需要通过连接设备导出日志,并根据符号表解析崩溃堆栈,过程耗时且繁琐,无意中降低了测试人员的工作效率。
Sunglasses方案:在程序发生崩溃时实时捕获崩溃相关信息,提示并记录,方便测试同学快速把崩溃堆栈反馈给研发同学,省去日志导出和崩溃解析的时间。
效果展示:
OC中方法调用最终都会转换为objc_msgSend(id self,SEL _cmd,...),整体会经历三个阶段:
消息发送:在类和父类的cache列表及方法列表中查找方法
动态解析:消息发送阶段没有找到方法,动态解析阶段动态添加
消息转发:未实现动态解析,消息进入转发阶段,可转发给可处理消息的接受者来处理。
如果上述三个阶段都未找到,则会触发 unrecognzied selector sent to instance
接下来,我们结合Runtime源码分析其内部实现。
1.1.1 方法查找
我们先看下整体流程图:
通过流程图我们可以看到首先会进行nil检测,源码中也会涉及对Tagged Pointer检测(Tagged Pointer此次暂不涉及)。此后会检测缓存是否存在调用方法,存在调用,不存在继续查找过程,流程图最终会调用到 lookUpImpOrForward方法,此方法也是查找的核心函数,接下来我们分析下该核心方法做了哪些操作。
lookUpImpOrForward 函数
首先看下此函数的整体流程图:
下面我们在通过源码看一下Runtime底层的优化点:
/***********************************************************************
* getMethodNoSuper_nolock
* fixme
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
// 方法有序的情况下,采用二分查找,提高查找效率
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
/*
二分查找通过位移运算进行
*/
for (count = list->count; count != 0; count >>= 1) {
// middle
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
// 二分操作
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
// Try superclass caches and method lists.
{
/*
类方法列表中没有找到方法,需要继续在父类的cache以及method list中进行查找
*/
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
/*
父类缓存中找到方法,进行缓存,缓存的地点是当前类的缓存列表中,并没有缓存到父类的缓存中
*/
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
1.1.2 动态解析
动态解析部分的流程图如下:
/* 动态解析 */
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES; // 标识是否进行过动态解析
goto retry; // 重新调用retry逻辑
}
动态解析只进行一次,如果进行动态解析,并实现对应的方法,这时会重新调用retry。
/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod.
* Returns nothing; any result would be potentially out-of-date already.
* Does not check if the method already exists.
**********************************************************************/
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
通过源码我们得知在动态解析时会区分类方法和对象方法,分别会调用resolveClassMethod 和 resolveInstanceMethod两个方法,在源码中类方法的逻辑中最终如果没有找到方法,会调用resolveInstanceMethod方法。这里做个解释:对象方法和类方法是存储在不同的位置。对象方法存在类的方法列表中,类方法存在元类的方法列表中,没有找到对应的方法时会通过继承关系最终找到NSObject,类方法依旧是如此。通过下图中的关系我们可以看到所有元类最终的父类都是NSObject,NSObject的最终父类是nil。同时方法存储在方法列表中是没有 “+” “-”之分的,所以方法最终查找到NSObject时就需要使用 resolveInstanceMethod 来查找。
动态解析时会涉及到如下两个方法:
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
1.1.3 消息转发
消息转发阶段涉及到的方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation
针对消息转发这里,源码中没有开源,我们通过查找最终定位到如下:
// Default forward handler halts the process.
__attribute__((noreturn)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
从源码中我们只能定位到一个错误信息的log。不过消息转发的整个流程是我们是了解的:
forwardingTargetForSelector
可以选择备用接收者进行消息处理
methodSignatureForSelector
forwardInvocation
上面这两个方法是结合来使用的,methodSignatureForSelector返回相应的方法签名,forwardInvocation进行消息转发的处理。
02Method Swizzle
1.2.1 实现原理
1.2.2 代码实现
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 原方法名和替换方法名
SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
// 原方法结构体和替换方法结构体
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 如果当前类没有原方法的实现IMP,先调用class_addMethod来给原方法添加默认的方法实现IMP
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {// 添加方法实现IMP成功后,修改替换方法结构体内的方法实现IMP和方法类型编码TypeEncoding
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else { // 添加失败,调用交互两个方法的实现
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
我们可以看到一共使用三个方法:
1.2.3 源码展示
/***********************************************************************
* class_addMethod
**********************************************************************/
static IMP _class_addMethod(Class cls, SEL name, IMP imp,
const char *types, bool replace)
{
old_method *m;
IMP result = nil;
if (!types) types = "";
mutex_locker_t lock(methodListLock);
if ((m = _findMethodInClass(cls, name))) {
// already exists
// fixme atomic
result = method_getImplementation((Method)m);
if (replace) {
method_setImplementation((Method)m, imp);
}
} else {
// fixme could be faster
old_method_list *mlist =
(old_method_list *)calloc(sizeof(old_method_list), 1);
mlist->obsolete = fixed_up_method_list;
mlist->method_count = 1;
mlist->method_list[0].method_name = name;
mlist->method_list[0].method_types = strdup(types);
mlist->method_list[0].method_imp = imp;
_objc_insertMethods(cls, mlist, nil);
if (!(cls->info & CLS_CONSTRUCTING)) {
flush_caches(cls, NO);
} else {
// in-construction class has no subclasses
flush_cache(cls);
}
result = nil;
}
return result;
}
/***********************************************************************
* class_addMethod
**********************************************************************/
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
IMP old;
if (!cls) return NO;
old = _class_addMethod(cls, name, imp, types, NO);
return !old;
}
/***********************************************************************
* class_replaceMethod
**********************************************************************/
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return nil;
return _class_addMethod(cls, name, imp, types, YES);
}
void method_exchangeImplementations(Method m1_gen, Method m2_gen)
{
IMP m1_imp;
old_method *m1 = oldmethod(m1_gen);
old_method *m2 = oldmethod(m2_gen);
if (!m1 || !m2) return;
impLock.lock();
m1_imp = m1->method_imp;
m1->method_imp = m2->method_imp;
m2->method_imp = m1_imp;
impLock.unlock();
通过源码我们可以清晰的看到实现原理,addMethod和和和replaceMethod都会调用_class_addMethod,该方法里面两种操作一种是设置 IMP指针,一个是添加方法,添加到对应的MethodList。method_exchangeImplementations方面里面就是交换方法的IMP指针。
官方解释: Class clusters are a design pattern that the Foundation framework makes extensive use of. Class clusters group a number of private concrete subclasses under a public abstract superclass. The grouping of classes in this way simplifies the publicly visible architecture of an object-oriented framework without reducing its functional richness. Class clusters are based on the Abstract Factory design pattern
大致意思:
类簇是一种Foundation框架经常使用的设计模式(desigen pattern)。类簇聚合类在一个共有抽象基类下的一些私有的具体的子类。用这种方法组合的类简化了面向对象(object-oriented)框架的共有的可用的结构,同时并没有减少它的功能的丰富性,类簇是基于抽象工厂设计模式的 (Abstract Factory design pattern)。
类簇设计理念:Simple Concept and Simple Interface(简单的概念,简单的接口)
关于NSNotification
If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method. Otherwise, you should call this method or removeObserver: before observer or any object specified in addObserverForName:object:queue:usingBlock: or addObserver:selector:name:object: is deallocated.
利用Method Swizzle,来完成异常处理机制,从而防止crash,并且提供相应提示。
2.2.1 集合类
NSArray,NSMutableArray等
针对类簇进行hook,要注意的问题:
2.2.2 Unrecognized Selector Sent to Instance
通过之前的Runtime源码分析我们可以了解到,找不到方法,还有三次补救的机会,分别为
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
(id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
针对这种情况我们hook了两个方法:
+ (NSMethodSignature*)hook_methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature* methodSignature = [self hook_classMethodSignatureForSelector:aSelector];
if (methodSignature) {
return methodSignature;
}
// 随意返回签名,保证进入invocation
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
我们返回了一个v@:的方法签名,会直接触发forwardInvocation,在forwardInvocation方法中我们不会再去触发系统的API,直接选择自己处理异常,进行相应的异常展示。
参考文献
Runtime源码:
https://opensource.apple.com/tarballs/objc4/
类簇私有类收集:
https://gist.github.com/Catfish-Man/bc4a9987d4d7219043afdf8ee536beb2
Class Clusters:
https://gist.github.com/Catfish-Man/bc4a9987d4d7219043afdf8ee536beb2
Type Encodings:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
Concepts in Objective-C Programming:
https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/ClassClusters/ClassClusters.html
本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/3rPu0iZASwW7EOMQHLP50w
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。