iOS组件化实践之:遇到的问题及解决过程纪要(持续更新中)
一、编译错误
1.1 MsgHandlingError
1
2
Build service could not create build operation: unknown error while handling message: MsgHandlingError(message: "unable to initiate PIF transfer session (operation in progress?)")
经Google之后,解决方案是要清楚编译构建索引,由于还是比较频繁出现,于是写了个脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/bin/bash
# [背景]
# 组件化过程中,经常出现以下编译错误:
# Build service could not create build operation: unknown error while handling message: MsgHandlingError(message: "unable to initiate PIF transfer session (operation in progress?)")
#
# author: Chenyp34
# [目的]
# 为解决上述错误,需要清理构建索引目录并重启xcode,由于操作重复,考虑抽出shell脚本
#
# [目录]
# - auto_clean.sh
# [说明]
# 1、CD[指定xcode构建索引目录]目录
# 2、递归删除当前文件下所有文件
# 3、重启xcode
#
# [参数]
# 1、参数1,指定xcode构建索引目录
# 1、参数2,指定要打开的Xcode项目目
#
# [调用说明]
# 1、未指定目录,则取当前目录:sh auto_clean.sh
# 2、指定目录:sh auto_clean.sh /xcode构建索引目录/ /要打开的Xcode项目目录/
export XcodeDerivedDataPath=/Users/YourMacName/Library/Developer/Xcode/DerivedData
export XcodeWorkspacePath=/Users/YourMacName/Documents/PCS/git/Project/xxx.xcworkspace
if [ -z "$1" ]; then
echo '///入参1为空,使用默认xcode构建索引目录:'${XcodeDerivedDataPath}
else
XcodeDerivedDataPath=$1
echo '///入参1不为空,xcode构建索引目录:'${XcodeDerivedDataPath}
fi
if [ -z "$2" ]; then
echo '///入参2为空,使用默认xcode项目目录:'${XcodeWorkspacePath}
else
XcodeWorkspacePath=$2
echo '///入参2不为空,xcode项目目录:'${XcodeWorkspacePath}
fi
#cd到xcode构建索引目录
cd $XcodeDerivedDataPath
#删除当前目录所有构件索引
rm -rf *
#重启xcode
killall Xcode
#休眠一下,否则额可能直接打开会报错
sleep 0.1
#打开工程
open $XcodeWorkspacePath
1.2 duplicate interface definition for class
1
duplicate interface definition for class XXX
排除了报错明面上的重复定义的可能性之后,后来查资料发现是import 问题,删除掉原先的import “xxx.h”,改成 import <PSCXXModule/xxx.h>
二、运行时错误
2.1 +(void)load方法交换导致死循环
2.1.1 问题说明:
在实施组件化过程之中,一个UIControl的Category与UIButton的Category,由于后者的耦合度较大,将前者先拆进组件库,编译没有问题,运行没有问题,只有在UIButton点击事件时造成了死循环。
2.1.2 问题分析:
通过Craash堆栈看出是以下这个方法递归调用造成死循环
- (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
详细排查代码,看到UIControl的Category与UIButton的Category写了一模一样的 load方法交换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL selA = @selector(sendAction:to:forEvent:);
SEL selB = @selector(mySendAction:to:forEvent:);
Method methodA = class_getInstanceMethod(self,selA);
Method methodB = class_getInstanceMethod(self, selB);
//将 methodB的实现 添加到系统方法中 也就是说 将 methodA方法指针添加成 方法methodB的 返回值表示是否添加成功
BOOL isAdd = class_addMethod(self, selA, method_getImplementation(methodB), method_getTypeEncoding(methodB));
//添加成功了 说明 本类中不存在methodB 所以此时必须将方法b的实现指针换成方法A的,否则 b方法将没有实现。
if (isAdd) {
class_replaceMethod(self, selB, method_getImplementation(methodA), method_getTypeEncoding(methodA));
}else{
//添加失败了 说明本类中 有methodB的实现,此时只需要将 methodA和methodB的IMP互换一下即可。
method_exchangeImplementations(methodA, methodB);
}
});
}
(姑且先不吐槽cv大法)
为何在组件化之前不会死循环,在组件化之后造成了死循环?
2.1.3 问题定位:
通过控制变量法可以初步判断:由于Pod集成方式改变造成的crash
load方法,是类加载时的调用,怀疑是加载时序发生了变化,导致crash暴露出来
随即展开类加载实验:主工程及Pod的类加载探索:
存在Pod库的类加载:
1
2
3
4
5
6
7
8
9
10
11
2023-06-28 09:19:26.104245+0800 TestUI[2809:1230318] [+ (void)load]:[Pod] UIControl (ClickTimeSpace) load
2023-06-28 09:19:26.108615+0800 TestUI[2809:1230318] [+ (void)load]:[Pod] UIImage (Bundle) load
2023-06-28 09:19:26.108626+0800 TestUI[2809:1230318] [+ (void)load]:[Pod] UIImage (Custom) load
2023-06-28 09:19:26.108637+0800 TestUI[2809:1230318] [+ (void)load]:[Pod] UIImageView (YPCCreateQRCode) load
2023-06-28 09:19:26.108647+0800 TestUI[2809:1230318] [+ (void)load]:[Pod] UILabel (YPCSpaceLabel) load
2023-06-28 09:19:26.108670+0800 TestUI[2809:1230318] [+ (void)load]:[Pod] UIResponder (KTRouter) load
2023-06-28 09:19:26.110711+0800 TestUI[2809:1230318] [+ (void)load]:[Project]NSObject (Sb) load
2023-06-28 09:19:26.110743+0800 TestUI[2809:1230318] [+ (void)load]:[Project]UITextField (Sub) load
2023-06-28 09:19:26.110777+0800 TestUI[2809:1230318] [+ (void)load]:[Project]UIResponder (Super) load
2023-06-28 09:19:26.110787+0800 TestUI[2809:1230318] [+ (void)load]:[Project]UIControl (Test) load
2023-06-28 09:19:26.114212+0800 TestUI[2809:1230318] [+ (void)load]:[Project]UIView (Base) load
只有主工程的类加载:
1
2
3
4
5
6
7
8
2023-06-28 09:25:27.432494+0800 TestUI[2821:1235269] [+ (void)load]:[Project]UIImageView (SubImgV) load
2023-06-28 09:25:27.432528+0800 TestUI[2821:1235269] [+ (void)load]:[Project]NSObject (Sb) load
2023-06-28 09:25:27.432559+0800 TestUI[2821:1235269] [+ (void)load]:[Project]UITextField (Sub) load
2023-06-28 09:25:27.432570+0800 TestUI[2821:1235269] [+ (void)load]:[Project]UITextView (SubTv) load
2023-06-28 09:25:27.432580+0800 TestUI[2821:1235269] [+ (void)load]:[Project]UIButton (Test) load
2023-06-28 09:25:27.432592+0800 TestUI[2821:1235269] [+ (void)load]:[Project]UIResponder (Super) load
2023-06-28 09:25:27.432602+0800 TestUI[2821:1235269] [+ (void)load]:[Project]UIControl (Test) load
2023-06-28 09:25:27.435639+0800 TestUI[2821:1235269] [+ (void)load]:[Project]UIView (Base) load
通过上述日志打印得出:Pod库里的类加载先于主工程
因此结论:在主工程时,先加载了UIButton的load方法,而UIButton无此方法:
1
2
//在UIControl存在此方法,而UIButton无此方法:
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
在先交换UIButton中的sendAction之后,再交换UIControl中的sendAction,sendAction方法能被正常交换
反之:在Pod工程中,先交换了UIControl中的sendAction之后,再交换UIButton中的sendAction,执行两次相同交换等于恢复了没有交换,当方法调用时:
1
2
3
4
5
6
7
//当我们按钮点击事件 sendAction 时 将会执行 mySendAction
- (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
//xxx
//此处 methodA和methodB方法IMP互换了,实际上执行 sendAction;所以不会死循环
[self mySendAction:action to:target forEvent:event];
}
无法回到sendAction,造成了死循环详细排查代码,看到UIControl的Category与UIButton的Category写了一模一样的 load方法交换: