前言
问题:升级最新IDE Xcode,发现app首页的cell中按钮也无法点击了。
原因:往cell添加子视图的方式不规范,导致contentView 置于自定义控件的上层,引发界面无响应(注意处理相关方法)
I 问题分析
iOS14 UITableViewCell的子试图不能点击或者滑动等手势响应问题,发现有问题的cell基本都是直接
代码语言:javascript复制cell.addSubView(tempView1)
这种方式添加的,通过Xcode自带的DebugViewHierarchy视图分析发现问题的原因是:被系统自带的UITableViewCellContentView遮挡在底部了
所以需要改规范的做法
代码语言:javascript复制cell.contentView.addSubView(tempView1)
温馨提示:如果你用旧版的Xcode打包,而非使用Xcode12以上版本编译打包的话,是不会有问题。一旦你使用了Xcode12打包,就会出现此问题。(但是苹果迟早会限制高于Xcode12才可以上传appstore,所以一旦使用了不规范的代码,早晚都要面临这个问题
)
1.1 其他分析视图层级的方法:私有API _printHierarchy 和recursiveDescription
关于视图层级分析你也可以使用私有API _printHierarchy
和recursiveDescription 在
lldb 窗口进行分析:
例如先打印VC层级 (lldb) po [[[UIWindow keyWindow] rootViewController] _printHierarchy]
再使用目标View的地址进行recursiveDescription
打印子视图的层级。
po [0x10ff5e5e0 recursiveDescription]
(lldb) po [0x10ff5e5e0 recursiveDescription]
<UITableViewCell: 0x10ff5e5e0; frame = (0 767.5; 375 120); hidden = YES; autoresize = W; layer = <CAGradientLayer: 0x280b80860>>
| <_UISystemBackgroundView: 0x10fe2d170; frame = (0 0; 375 120); layer = <CAGradientLayer: 0x280c58500>; configuration = <UIBackgroundConfiguration: 0x283aa54a0; Base Style = List Grouped Cell; backgroundColor = <UIDynamicSystemColor: 0x2818d3140; name = tableCellGroupedBackgroundColor>>>
| | <UIView: 0x10fe2d310; frame = (0 0; 375 120); clipsToBounds = YES; layer = <CAGradientLayer: 0x280c58640>>
| <UIView: 0x10ff9a820; frame = (0 0; 375 120); layer = <CAGradientLayer: 0x280b9db60>>
| | <UIButton: 0x10ff9ab10; frame = (17 0; 170.5 60); opaque = NO; layer = <CAGradientLayer: 0x280b9dc40>>
| | | <UIImageView: 0x10fe70710; frame = (0 16; 28 28); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280be9520>>
| | | <UIButtonLabel: 0x10ff9af70; frame = (38 21.5; 86 17); text = '商户交易汇总'; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b9dc80>>
| | <UIButton: 0x10ff9bd40; frame = (187.5 0; 170.5 60); opaque = NO; tag = 1; layer = <CAGradientLayer: 0x280b9e1c0>>
| | | <UIImageView: 0x10ffacfd0; frame = (0 16; 28 28); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b93220>>
| | | <UIButtonLabel: 0x10ff9c1a0; frame = (38 21.5; 100 17); text = '代理商交易汇总'; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b9e340>>
| | <UIButton: 0x10ff9cda0; frame = (17 60; 170.5 60); opaque = NO; tag = 2; layer = <CAGradientLayer: 0x280b9e540>>
| | | <UIImageView: 0x10ffab1f0; frame = (0 16; 28 28); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b92f40>>
| | | <UIButtonLabel: 0x10ff9d200; frame = (38 21.5; 86 17); text = '终端激活汇总'; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b9e680>>
| | <UIButton: 0x10ff9db20; frame = (187.5 60; 170.5 60); opaque = NO; tag = 3; layer = <CAGradientLayer: 0x280b9ea20>>
| | | <UIImageView: 0x10ffa95d0; frame = (0 16; 28 28); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b92ce0>>
| | | <UIButtonLabel: 0x10ff9df80; frame = (38 21.5; 86 17); text = '商户终端汇总'; opaque = NO; userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b9eb80>>
| | <UIView: 0x10ff9e8a0; frame = (15 60; 345 0.5); layer = <CAGradientLayer: 0x280b9ef00>>
| | <UIView: 0x10ff9ea10; frame = (15 120; 345 0.5); layer = <CAGradientLayer: 0x280b9f0c0>>
| <UITableViewCellContentView: 0x10ffaafa0; frame = (0 0; 375 120); gestureRecognizers = <NSArray: 0x281fc73c0>; layer = <CAGradientLayer: 0x280b808e0>>
| | <UIImageView: 0x110f07d00; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <CAGradientLayer: 0x280b7e220>>
1.2 注意事项
因为此问题涉及的是添加子视图cell.addSubView
,因此与之对应的方法(UITableViewCell *)[SubView superview]
和cell.subviews
都要注意谨慎使用和处理
II 解决UITableViewCell兼容问题
如果错误代码比较多,可以采用hook,进行便捷的方法进行修改。
例如125个文件的1452个地方使用错误的方法,这个如果不使用hook高质工作量有点大
所以通过Runtime hook cell的addSubView 方法强制修改为正确的添加cell 子视图的方式
2.1 全局修改
- 只允许添加 UITableViewCellContentView,其余都直接添加到self.contentView
//
// UITableViewCell CRMaddSubView.m
// Housekeeper
//
// Created by mac on 2020/9/18.
// Copyright © 2020 QCT. All rights reserved.
//
#import "UITableViewCell CRMaddSubView.h"
@implementation UITableViewCell (CRMaddSubView)
(void)load {
// Swizzle addSubView
[UITableViewCell sensorsdata_swizzleMethod:@selector(addSubview:) withMethod:@selector(kunnan_addSubview:)];
}
- (void)kunnan_addSubview:(UIView *)view {
if ([view isKindOfClass:NSClassFromString(@"UITableViewCellContentView")]) {//允许 addSubView UITableViewCellContentView
[self kunnan_addSubview:view];//实现方法,因为已经进行了 swizzle,相当于调用原来的方法
} else {//
[self.contentView addSubview:view];
}
}
@end
2.2 问题:使用文件预浏界面的打印功能,闪退。
原因:由于上面的分类只对UITableViewCellContentView进行判断,忽略了其他contentView类型,导致把自己添加到自己的情况。
UIPrintOptionCell的contentView是UIListContentView
解决方式:如果子类名称包含ContentView就不处理,不包含ContentView才将其添加到cell。
代码语言:javascript复制#import "UITableViewCell CRMaddSubView.h"
@implementation UITableViewCell (CRMaddSubView)
(void)load {
// return;
// Swizzle addSubView
[UITableViewCell sensorsdata_swizzleMethod:@selector(addSubview:) withMethod:@selector(kunnan_addSubview:)];
}
- (void)kunnan_addSubview:(UIView *)view {
if ([view isKindOfClass:NSClassFromString(@"UITableViewCellContentView")] || [NSStringFromClass(view.class) containsString:@"ContentView"]) {
[self kunnan_addSubview:view];//
} else {//@interface UIListContentView : UIView <UIContentView>
[self.contentView addSubview:view];//UIPrintOptionCell
}
}
2.3 注意事项
因为此问题涉及的是添加子视图cell.addSubView,因此与之对应的方法(UITableViewCell *)[SubView superview] 和cell.subviews 都要注意谨慎使用和处理
具体例子如下2.3.1 和2.3.2
2.3.1 cell.subviews
因为这是针对全局的,所以测试的覆盖面也要广。 比如获取子视图采用cell.subviews 也要记得修改为 cell.contentView.subviews
.
UIButton * btn = cell.contentView.subviews[2-1];
2.3.2 通过superview 获取cell的也需做相关修改
- 经过全局hook之后,以下的代码就是错误的
(UITableViewCell *)[textField superview]
- 全局搜索进行修改
UITableViewCell * myCell = (UITableViewCell *)[textField superview].superview;
所以使用class的时候,最好写得健壮性强点,进行类型判断,避免一旦类型错误,就会找不到对应的方法,发送闪退
代码语言:javascript复制 UIView * textFieldsuperview = [textField superview];
UITableViewCell * myCell = nil;
if([textFieldsuperview isKindOfClass:NSClassFromString(@"UITableViewCellContentView")]){
myCell= (UITableViewCell *)[textFieldsuperview superview];
}else{
return;
}
能遇见这样的奇葩代码,只能说之前的同事很”牛逼啊。。。。“
2.4 使用到的工具类
- h
//
// NSObject CRMSwizzling.h
// Housekeeper
//
// Created by mac on 2020/9/18.
// Copyright © 2020 QCT. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef IMP *IMPPointer;
/**
让所有继承自NSObject的子类,都具有Method Swizzling的能力。
*/
@interface NSObject (CRMSwizzling)
/**
交换方法名为 originalSEL 和方法名为 alternateSEL 两个方法的实现
@param originalSEL 原始方法名
@param alternateSEL 要交换的方法名称
*/
(BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL;
/**
方式二
*/
(BOOL)swizzle:(SEL)original with:(IMP)replacement store:(out IMPPointer)store;
@end
NS_ASSUME_NONNULL_END
- m
//
// NSObject CRMSwizzling.m
// Housekeeper
//
// Created by mac on 2020/9/18.
// Copyright © 2020 QCT. All rights reserved.
//
#import <objc/runtime.h>
#import <objc/message.h>
#import "NSObject CRMSwizzling.h"
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store);
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) {
*store = imp;
}
return (imp != NULL);
}
@implementation NSObject (CRMSwizzling)
(BOOL)sensorsdata_swizzleMethod:(SEL)originalSEL withMethod:(SEL)alternateSEL {
// 获取原始方法
Method originalMethod = class_getInstanceMethod(self, originalSEL);
// 当原始方法不存在时,返回 NO,表示 Swizzling 失败
if (!originalMethod) {
return NO;
}
// 获取要交换的方法
Method alternateMethod = class_getInstanceMethod(self, alternateSEL);
// 当要交换的方法不存在时,返回 NO,表示 Swizzling 失败
if (!alternateMethod) {
return NO;
}
// 获取 originalSEL 方法的实现
IMP originalIMP = method_getImplementation(originalMethod);
// 获取 originalSEL 方法的类型
const char * originalMethodType = method_getTypeEncoding(originalMethod);
// 往类中添加 originalSEL 方法,如果已经存在会添加失败,并返回 NO
if (class_addMethod(self, originalSEL, originalIMP, originalMethodType)) {
// 如果添加成功了,重新获取 originalSEL 实例方法
originalMethod = class_getInstanceMethod(self, originalSEL);
}
// 获取 alternateIMP 方法的实现
IMP alternateIMP = method_getImplementation(alternateMethod);
// 获取 alternateIMP 方法的类型
const char * alternateMethodType = method_getTypeEncoding(alternateMethod);
// 往类中添加 alternateIMP 方法,如果已经存在会添加失败,并返回 NO
if (class_addMethod(self, alternateSEL, alternateIMP, alternateMethodType)) {
// 如果添加成功了,重新获取 alternateIMP 实例方法
alternateMethod = class_getInstanceMethod(self, alternateSEL);
}
// 交换两个方法的实现
method_exchangeImplementations(originalMethod, alternateMethod);
// 返回 YES,表示 Swizzling 成功
return YES;
}
(BOOL)swizzle:(SEL)original with:(IMP)replacement store:(out IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
III 逆向相关
3.1 hopper 修改汇编的方式:
选中行,选择菜单栏的Modify > Assemble Instruction…,将jne修改成je,然后点击Assemble and Go Next。
3.2 iOS 恢复调用栈(适配iOS14)
原理:objective-c 函数信息除了保存在符号表中,还保存在其他段中
https://github.com/zhangkn/restore-symbol4iOS14__TEXT.__objc_methname - Method names for locally implemented methods __TEXT.__objc_classname - Names for locally implemented classes __TEXT.__objc_methtype - Types for locally implemented method types __DATA.__objc_classlist - An array of pointers to ObjC classes __DATA.__objc_nlclslist - An array of pointers to classes who implement load __DATA.__objc_catlist - List of ObjC categories __DATA.__objc_protolist - List of ObjC protocols __DATA.__objc_imageinfo - Version info, not really useful __DATA.__objc_const - Constant data, i.e. class_ro_t data __DATA.__objc_selrefs - External references to selectors __DATA.__objc_protorefs - External references to protocols __DATA.__objc_classrefs - External references to other classes __DATA.__objc_superrefs - External references to super classes __DATA.__objc_ivar - Offsets to ObjC properties __DATA.__objc_data - Misc ObjC storage, notably ObjC classes
see also
代码语言:javascript复制extension UITableViewCell {
class func ios14Bug() {
let sel1 = #selector(UITableViewCell.runtime_addSubview(_:))
let sel2 = #selector(UITableViewCell.addSubview(_:))
let method1 = class_getInstanceMethod(UITableViewCell.self, sel1)!
let method2 = class_getInstanceMethod(UITableViewCell.self, sel2)!
let isDid: Bool = class_addMethod(self, sel2, method_getImplementation(method1), method_getTypeEncoding(method1))
if isDid {
class_replaceMethod(self, sel1, method_getImplementation(method2), method_getTypeEncoding(method2))
} else {
method_exchangeImplementations(method2, method1)
}
}
@objc func runtime_addSubview(_ view: UIView) {
// 判断不让 UITableViewCellContentView addSubView自己//需要新增判断条件,请看本文的2.2章节 if view.isKind(of: NSClassFromString("UITableViewCellContentView")!) {
runtime_addSubview(view)
} else {
self.contentView.addSubview(view)
}
}
}
还发现他的另一个不规范使用cell API导致的问题,具体请看这里