iOS约束冲突查找调试工具

Posted 想名真难

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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

以上是关于iOS约束冲突查找调试工具的主要内容,如果未能解决你的问题,请参考以下文章

堆栈视图中的自动约束冲突、Swift 2、iOS 9.3、XCode 7

SnapKit 更新约束导致冲突

iOS - 查找视图的顶部约束?

如何在iOS中调试布局约束问题?

iOS:转储视图和约束的层次结构树

iOS 8 键盘扩展:出现呼叫栏时出现约束错误?