iOS学习专题iOS进阶指南零碎知识点

约束冲突调试工具:解决iOS7调试难题

2016-12-13  本文已影响636人  黑超熊猫zuik

功能

现状

iOS7对Auto Layout的支持问题

iOS7的调试问题

解决思路

如果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的方法有两种。

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?

我选择了第二种方式。

监测iOS7约束导致的crash

当你在实现自定义view的layoutSubviews方法时,记住:

如果不遵守第一条,当你向这个view上增加子view时,在iOS6和iOS7上会crash,控制台会输出提示:'Auto Layout still required after executing - layoutSubviews..' 。iOS8开始则不会crash。如果不遵守第二条,iOS7以下会发生死循环。

某些系统控件,例如UITableViewUITableViewCell没有调用[super layoutSubviews],所以在iOS6和iOS7上不能在它们上面增加子view,除非你用method swizlling修复它们的layoutSubviews方法。

经过反编译分析,'Auto Layout still required after executing - layoutSubviews..'发生在UIViewlayoutSublayersOfLayer:里,发生错误之前会用-[UIView _wantsWarningForMissingSuperLayoutSubviews]来监测是否调用了[super layoutSubviews],如果没有则抛出异常。
因此只需要hook_wantsWarningForMissingSuperLayoutSubviews就可以了。

最终效果

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

    [ZIKConstraintsGuard monitorUnsatisfiableConstraintWithHandler:^(UIView *view, UIViewController *viewController, NSLayoutConstraint *constraintToBreak, NSArray<NSLayoutConstraint *> *currentConstraints) {
        NSLog(@"检测到约束冲突!");
        NSString *className = NSStringFromClass([viewController class]);
        if ([className hasPrefix:@"UI"] && ![className isEqualToString:@"UIApplication"]) {
              //使用某些系统控件时会出现约束冲突,例如UIAlertController
            NSLog(@"ignore conflict in UIKit:%@",viewController);
            return;
        }
        NSLog(@"冲突所在的viewController:\n%@ \nview:\n%@",viewController,view);
        //使用recursiveDescription来打印view的层级,注意这是private API
        NSLog(@"view hierarchy:\n%@",[view 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)>

这样就能根据记录到的内存地址,准确地找到是哪个界面的哪个控件的约束出错了,即便在iOS7上crash,也能在crash之前记录到错误信息。

需要注意的问题

源代码

工具地址在此:ZIKConstraintsGuard

上一篇 下一篇

猜你喜欢

热点阅读