iOS约束冲突查找调试工具

在开发过程中,经常会在控制台看到系统输出这样的约束冲突

**Unable to simultaneously satisfy constraints.**
    Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
    "<NSLayoutConstraint:0x7fc82d3e18a0 H:[UIView:0x7fc82aba1210(768)]>",
    "<NSLayoutConstraint:0x7fc82d6369e0 H:[UIView:0x7fc82aba1210]-(0)-|   (Names: '|':UIView:0x7fc82d6b9f80 )>",
    "<NSLayoutConstraint:0x7fc82d636a30 H:|-(0)-[UIView:0x7fc82aba1210]   (Names: '|':UIView:0x7fc82d6b9f80 )>",
    "<NSLayoutConstraint:0x7fc82d3e7fd0 'UIView-Encapsulated-Layout-Width' H:[UIView:0x7fc82d6b9f80(50)]>"
)

Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fc82d3e18a0 H:[UIView:0x7fc82aba1210(768)]>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

在解完一波冲突之后,过了一段时间发现又出现了新的约束冲突,如果能在开发的过程中就发现并解决这些约束冲突,并且后续合入主分支的代码中不带有这些约束冲突,就需要想办法找到这样log的出现时机,在开发过程中添加断言,避免此类问题重复出现。

功能

  • 在非调试模式下,获取出错的具体约束。
  • 监测约束冲突,并获取出错的view和viewController。

解决思路

如果app能用代码监测到约束冲突,就可以在非调试模式下捕获到有用的信息,帮助快速定位问题。
当发生约束冲突时,控制台会输出这样的提示:

**Unable to simultaneously satisfy constraints.**
    Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
    "<NSLayoutConstraint:0x7fc82d3e18a0 H:[UIView:0x7fc82aba1210(768)]>",
    "<NSLayoutConstraint:0x7fc82d6369e0 H:[UIView:0x7fc82aba1210]-(0)-|   (Names: '|':UIView:0x7fc82d6b9f80 )>",
    "<NSLayoutConstraint:0x7fc82d636a30 H:|-(0)-[UIView:0x7fc82aba1210]   (Names: '|':UIView:0x7fc82d6b9f80 )>",
    "<NSLayoutConstraint:0x7fc82d3e7fd0 'UIView-Encapsulated-Layout-Width' H:[UIView:0x7fc82d6b9f80(50)]>"
)

Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fc82d3e18a0 H:[UIView:0x7fc82aba1210(768)]>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

提示我们在UIViewAlertForUnsatisfiableConstraints上打断点调试。
这是一个检测到出错约束时,进行处理的C函数。上面那串控制台的log就是在这个函数里输出的。

于是可以尝试用method swizzling替换系统库的方法,记录出现冲突时的信息。

实现方法

获取UIView

runtime无法替换C函数,而调用栈里NSISEngine的那几个方法都没附带什么有用的信息,于是用hopper反编译UIKit.framework,找到使用UIViewAlertForUnsatisfiableConstraints的地方,是-[UIView engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:]

这个方法附带了出错约束的信息,也可以获取到冲突所在的UIView,于是也能通过UIView获取对应的viewController。接下来只要hook这个方法就可以了。

获取view controller

获取view对应的view controller的方法有两种。

  • 使用UIView的私有API:_viewDelegate
  • 使用UIRespondernextResponder

The UIResponder class does not store or set the next responder automatically, instead returning nil by default. Subclasses must override this method to set the next responder. UIView implements this method by returning the UIViewController object that manages it (if it has one) or its superview (if it doesn’t); UIViewController implements the method by returning its view’s superview; UIWindow returns the application object, and UIApplication returns nil.

参考:Given a view, how do I get its viewController?

我选择了第二种方式。

//为UIView扩展一个方法,用于响应事件链
- (UIViewController *)viewController {

    UIResponder *nexRes=[self nextResponder];
    do {
        //判读当前的响应者是否UIViewController
        if ([nexRes isKindOfClass:[UIViewController class]]) {
            //是否直接处理
            return  (UIViewController*)nexRes;
        } else {
            //否则继续寻找
            nexRes=[nexRes nextResponder];
        }
    } while (nexRes!=nil);
    return nil;
}

最终效果

设置监听方式如下,返回约束冲突所在的view,viewController,系统尝试打破的约束,目前所有的约束。

在UIView类别中hook系统方法

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [AvoidCrash exchangeInstanceMethod:[self class]                                method1Sel:@selector(engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:)                                method2Sel:@selector(ul_engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:)];
    });
}

- (void)ul_engine:(id)engin willBreakConstraint:(NSLayoutConstraint *)constraint dueToMutuallyExclusiveConstraints:(NSArray <NSLayoutConstraint *> *)allConstraint {
    [self ul_engine:engin willBreakConstraint:constraint dueToMutuallyExclusiveConstraints:allConstraint];

    NSLog(@"检测到约束冲突!");
    NSString *className = NSStringFromClass([self.viewController class]);
    if ([className hasPrefix:@"UI"] && ![className isEqualToString:@"UIApplication"]) {
        //使用某些系统控件时会出现约束冲突,例如UIAlertController
        NSLog(@"ignore conflict in UIKit:%@",viewController);
        return;
    }
    NSLog(@"冲突所在的viewController:\n%@ \nview:\n%@",self.viewController,self);
    //使用recursiveDescription来打印view的层级,注意这是private API
    NSLog(@"view hierarchy:\n%@",[self valueForKeyPath:@"recursiveDescription"]);
    NSLog(@"目前所有的约束:\n%@",currentConstraints);
    NSLog(@"系统尝试打破的约束:\n%@",constraintToBreak);
}

打印结果如下:

检测到约束冲突!

冲突所在的viewController:
<MyViewController: 0x100201ba0> 
view:
<UIView: 0x10020cbb0; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x170242b50>; layer = <UIWindowLayer: 0x17002b240>>

view hierarchy:

<UIView: 0x10020cbb0; frame = (0 0; 375 667); autoresize = W+H; gestureRecognizers = <NSArray: 0x170242b50>; layer = <UIWindowLayer: 0x17002b240>>
   | <UIView: 0x10020fd00; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x17002b780>>
   |    | <_UILayoutGuide: 0x1002100a0; frame = (0 0; 0 0); hidden = YES; layer = <CALayer: 0x17002b820>>
   |    | <_UILayoutGuide: 0x100210650; frame = (0 0; 0 0); hidden = YES; layer = <CALayer: 0x17002b8e0>>
   |    | <UITableView: 0x10081cc00; frame = (100 100; 100 100); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x170243e70>; layer = <CALayer: 0x17002bf20>; contentOffset: {0, 0}; contentSize: {0, 0}>
   |    |    | <UITableViewWrapperView: 0x10080fe00; frame = (0 0; 100 100); gestureRecognizers = <NSArray: 0x1702441a0>; layer = <CALayer: 0x17002bf80>; contentOffset: {0, 0}; contentSize: {100, 100}>

目前所有的约束:
(
    "<NSLayoutConstraint:0x17008a500 UITableView:0x10081cc00.top == UITableView:0x10081cc00.top + 10   (active)>"
)

系统尝试打破的约束:
<NSLayoutConstraint:0x17008a500 UITableView:0x10081cc00.top == UITableView:0x10081cc00.top + 10   (active)>

这样就能根据记录到的内存地址,准确地找到是哪个界面的哪个控件的约束出错了。

需要注意的问题

  • 某些系统控件本身存在约束冲突的问题,例如在使用UIAlertController的时候。建议在检测到冲突时,再检测viewController的类型前缀,如果是UI前缀则忽略。其他不在UIKit里的系统控件,请自行判断。
  • 同一个约束冲突有时候会有多次回调。这些回调来自处理auto layout的不同阶段,例如添加重复约束时、addSubview时,layoutSubLayer时等。

源代码

工具地址在此:ZIKConstraintsGuard

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值