Измененный порядок загрузки + в Xcode 7

Я выяснил, что Xcode 7 (версия 7.0 (7A220)) изменил порядок, в котором методы +load для классов и категорий вызываются во время модульных тестов.

Если категория, принадлежащая тестовой цели, реализует метод +load, она теперь вызывается в конце, когда экземпляры класса могут быть уже созданы и использованы.

У меня есть AppDelegate, который реализует метод +load. Файл AppDelegate.m также содержит категорию AppDelegate (MainModule). Кроме того, существует unit test файл LoadMethodTestTests.m, который содержит другую категорию - AppDelegate (UnitTest).

Обе категории также реализуют метод +load. Первая категория относится к основной цели, вторая - к тестовой цели.

Код

Я сделал небольшой тестовый проект, чтобы продемонстрировать эту проблему. Это пустой проект по умолчанию для Xcode один, только с двумя измененными файлами.

AppDelegate.m:

#import "AppDelegate.h"

@implementation AppDelegate

+(void)load {
    NSLog(@"Class load");
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    NSLog(@"didFinishLaunchingWithOptions");

    return YES;
}

@end

@interface AppDelegate (MainModule)
@end

@implementation AppDelegate (MainModule)

+(void)load {
    NSLog(@"Main Module +load");
}

@end

И файл unit test (LoadMethodTestTests.m):

#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import "AppDelegate.h"

@interface LoadMethodTestTests : XCTestCase

@end

@interface AppDelegate (UnitTest)
@end

@implementation AppDelegate (UnitTest)

+(void)load {
    NSLog(@"Unit Test +load");
}

@end

@implementation LoadMethodTestTests

-(void)testEmptyTest {
    XCTAssert(YES);
}

@end

Тестирование

Я выполнил Unit Testing этого проекта (код и ссылка github ниже) на Xcode 6/7 и получил следующий порядок вызовов +load:

Xcode 6 (iOS 8.4 simulator):
    Unit Test +load
    Class load
    Main Module +load
    didFinishLaunchingWithOptions

Xcode 7 (iOS 9 simulator):
    Class load
    Main Module +load
    didFinishLaunchingWithOptions
    Unit Test +load

Xcode 7 (iOS 8.4 simulator):
    Class load
    Main Module +load
    didFinishLaunchingWithOptions
    Unit Test +load

Вопрос

Xcode 7 запускает метод целевой категории +load (Unit Test +load) в конце, после того, как AppDelegate уже создан. Правильно ли это, или это ошибка, которую следует отправить Apple?

Может быть, он не указан, поэтому компилятор/среда выполнения могут свободно переупорядочивать звонки? Я рассмотрел этот вопрос SO, а также описание + load в документации NSObject, но я не совсем понял, как должен работать метод +load, когда категория принадлежит другой цели.

Или может быть AppDelegate по какой-то причине является своего рода частным случаем?

Почему я спрашиваю об этом

  • Образовательные цели.
  • Я использовал метод swizzling в категории внутри цели unit test. Теперь, когда порядок вызова изменился, applicationDidFinishLaunchingWithOptions выполняется до того, как происходит swizzling. По-моему, есть другие способы сделать это, но для Xcode 7. мне кажется, что он интуитивно понятен, так как он работает в Xcode 7. Я думал, что когда класс загружается в память, +load этого класса и +load методы всех его категорий должны быть вызваны, прежде чем мы сможем что-то с этим классом (например, создать экземпляр и вызвать didFinishLaunching...).

Ответ 1

TL, DR: Это xctest ошибка, а не objc.

Это связано с тем, как исполняемый файл xctest (тот, который фактически запускает модульные тесты, расположенные в $XCODE_DIR/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Agents/xctest, загружает свой пакет.

Pre-Xcode 7, он загрузил пакеты all, указанные в тестовых пакетах, перед запуском любых тестов. Это можно увидеть (для тех, кто заботится), разобрав двоичный код для Xcode 6.4, соответствующий раздел можно увидеть для символа -[XCTestTool runTestFromBundle:].

В версии Xcode 7 xctest вы можете видеть, что она задерживает загрузку тестовых пакетов до тех пор, пока фактический тест не будет выполняться XCTestSuite, в фактической структуре xctest, которую можно увидеть в символе __XCTestMain, который вызывается только после установки тестового хост-приложения.

Поскольку порядок их вызывается внутренне изменен, способ, которым вы вызываете методы test +load, отличается. Не было внесено никаких изменений в внутренние функции objective-c -runtime.

Если вы хотите исправить это в своем приложении, вы можете сделать несколько вещей. Сначала вы можете вручную загрузить свой пакет с помощью +[NSBundle bundleWithPath:] и вызвать -load.

Вы также можете связать свою тестовую цель с тестовым хост-приложением (я надеюсь, что вы используете отдельный тестовый хост, чем основное приложение!), что сделает его автоматически загруженным, когда xctest загрузит хост-приложение.

Я бы не счел это ошибкой, это просто деталь реализации XCTest.

Источник: просто потратьте последние 3 дня на демонтаж xctest по совершенно не связанной причине.

Ответ 2

Xcode 7 имеет два разных порядка загрузки в проекте шаблона iOS.

Unit Test Case. Для Unit Test тестовый комплект вводится в текущую симуляцию после запуска приложения на главный экран. По умолчанию последовательность выполнения Unit Test выглядит следующим образом:

Application: AppDelegate initialize()
Application: AppDelegate init()
Application: AppDelegate application(…didFinishLaunchingWithOptions…)
Application: ViewController viewDidLoad()
Application: ViewController viewWillAppear()
Application: AppDelegate applicationDidBecomeActive(…)
Application: ViewController viewDidAppear()
Unit Test: setup()
Unit Test: testExample()

Тест пользовательского интерфейса.. Для тестирования пользовательского интерфейса создается отдельный второй процесс XCTRunner, который выполняет тестируемое приложение. Аргумент может быть передан из теста setUp()...

class Launch_UITests: XCTestCase {

  override func setUp() {
    // … other code …
    let app = XCUIApplication()
    app.launchArguments = ["UI_TESTING_MODE"]
    app.launch()
    // … other code …
  }

... для приема будет AppDelegate...

class AppDelegate: UIResponder, UIApplicationDelegate {

  func application(… didFinishLaunchingWithOptions… ) -> Bool {
    // … other code …
    let args = NSProcessInfo.processInfo().arguments
    if args.contains("UI_TESTING_MODE") {
        print("FOUND: UI_TESTING_MODE")
    }
    // … other code …

Впрыскивание в отдельный процесс можно наблюдать путем печати NSProcessInfo.processInfo().processIdentifier и NSProcessInfo.processInfo().processName как из тестового кода, так и из кода приложения.