是否可以调整 UIViewAlertForUnsatisfiableConstraints?

Is it possible to swizzle UIViewAlertForUnsatisfiableConstraints?

我想知道 UIViewAlertForUnsatisfiableConstraints 是否是一个我可以调整的选择器。到目前为止我所知道的是 UIViewAlertForUnsatisfiableConstraints 是一个可以用来放置符号断点的符号。但是我在任何 public class 中都找不到这个符号。有人知道它在哪里吗?

请注意,我希望在单元测试中而不是在生产代码中执行此操作。

由于 UIViewAlertForUnsatisfiableConstraints 是一个 C 函数,因此调配它需要比典型的 Objective C 方法多得多的工作。

最简单的(也是大多数消息来源所说的唯一可能的方法)是通过 Substrate 的 MSHookFunction,但是这需要越狱 phone。如果这是一个有效的选项,我建议使用它来简单地挂接函数。

在非越狱环境中,需要高级运行时操作。出于某种原因,我真的很喜欢浪费时间学习一门即将消失的语言。所以这里是完整的解决方案!这 link 帮了大忙。

#import <dlfcn.h>
#import <objc/runtime.h>
#include <sys/mman.h>

int64_t originalOffset;
int64_t *origFunc;

void swizzled_UIViewAlertForUnsatisfiableConstraints(NSLayoutConstraint *offendingConstraint, NSArray *allConstraints) {
    // inject swizzle code here
    NSLog(@"swizzled!");

    // call the original function (if you want!)
    if (origFunc) {
        // replace jump instruction w/ the original memory offset
        *origFunc = originalOffset;
        ((void(*)(NSLayoutConstraint*, NSArray*))origFunc)(offendingConstraint, allConstraints);
    }
}

static inline BOOL swizzleAlertForUnsatisfiableConstraints() {

    // get the original function and hold onto it's memory offset
    origFunc = dlsym(RTLD_DEFAULT, "UIViewAlertForUnsatisfiableConstraints");
    if (!origFunc) {
        return NO;
    }
    originalOffset = *origFunc;

    // define the swizzled implementation
    int64_t *swizzledFunc = (int64_t*)&swizzled_UIViewAlertForUnsatisfiableConstraints;

    // make the memory containing the original funcion writable
    size_t pageSize = sysconf(_SC_PAGESIZE);
    uintptr_t start = (uintptr_t)origFunc;
    uintptr_t end = start + 1;
    uintptr_t pageStart = start & -pageSize;
    mprotect((void *)pageStart, end - pageStart, PROT_READ | PROT_WRITE | PROT_EXEC);

    //Calculate the relative offset needed for the jump instruction
    //Since relative jumps are calculated from the address of the next instruction,
    //  5 bytes must be added to the original address (jump instruction is 5 bytes)
    int64_t offset = (int64_t)swizzledFunc - ((int64_t)origFunc + 5 * sizeof(char));

    //Set the first instruction of the original function to be a jump
    //  to the replacement function.
    //E9 is the x86 opcode for an unconditional relative jump
    int64_t instruction = 0xe9 | offset << 8;
    *origFunc = instruction;

    return YES;
}

我尝试了 Casey 的解决方案并且工作正常,但仅限于模拟器。 ARM 处理器有 Thumb 模式,所以使用 ARM B 指令比 x86 JMP 更难。

UIViewAlertForUnsatisfiableConstraints 从以下 UIView 方法调用。好像是在内部引擎的delegate里面定义的。

- (void)engine:(NSISEngine *)arg1 willBreakConstraint:(id <NSISConstraint>)arg2 dueToMutuallyExclusiveConstraints:(NSArray *)arg3;

https://github.com/nst/iOS-Runtime-Headers/blob/6a384f6a219be448de8714f263a4c212516d52b6/Frameworks/UIKit.framework/UIView.h#L1108

https://github.com/nst/iOS-Runtime-Headers/blob/3dde29deb7000cfac59d3e5473a71557123f29ea/protocols/NSISEngineDelegate.h#L10

调配 ObjC 方法比 C 函数容易得多。

定义私有接口:

@interface UIView (UIConstraintBasedLayout_EngineDelegate_PrivateInterface)

- (void)engine:(id /* NSISEngine */)engine willBreakConstraint:(NSLayoutConstraint *)breakConstraint dueToMutuallyExclusiveConstraints:(NSArray<NSLayoutConstraint *> *)mutuallyExclusiveConstraints;

@end

然后swizzle objc方法:

+ (void)swizzleAutoLayoutAlertMethod
{
    Class class = [UIView class];
    Method originalMethod = class_getInstanceMethod(class, @selector(engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:));
    if (!originalMethod) {
        [NSException raise:NSInternalInconsistencyException format:@"This platform does not support Auto Layout or interface has been changed."];
    }
    Method swizzledMethod = class_getInstanceMethod(class, @selector(swizzled_engine:willBreakConstraint:dueToMutuallyExclusiveConstraints:));
    method_exchangeImplementations(swizzledMethod, originalMethod);
}

注意:我用这个想法为自动布局创建了 lint 库,参考 https://github.com/ypresto/AutoLayoutLint .