background

Overview of iOS Category features

Categories are a language feature added after Objective-C 2.0.

A Category is a concrete implementation of the decoration pattern. Its main purpose is to dynamically add methods to a class without changing the original class. In Objective-C, instance (class) methods, attributes, and protocols.

In addition to the addition methods mentioned in the reference, categories have many advantages, such as splitting the implementation of a class into separate files, the ability to declare private methods, and even the ability to simulate multiple inheritance, as described in the official documentation Category.

If a Category is added to a method that already exists in the base class, the method of the same name will be overridden. Communication between components is implemented based on this feature, and coverage risk management is mentioned at the end of this article.

Component communication background

With the rapid development of mobile Internet, iterative mobile terminal engineering often faces common problems such as serious coupling, low maintenance efficiency, and less agile development. Therefore, more and more companies begin to implement “componentalization”, which can improve the efficiency of parallel development by decoupling and recombining components.

But what most teams call “componentization” is splitting code into libraries, and the main project uses the CocoaPods tool to aggregate the version numbers of each sub-library. But few companies can properly layer components and have a full toolchain to support release and integration, making it difficult to significantly improve development efficiency.

It is always difficult to deal with the communication and decoupling between components. Issues such as explicit Podfile dependencies between components and various joint releases can be disastrous if not handled properly.

At present, there are many teams decoupling at the ViewController level. They maintain a set of mapping relations and use Scheme to jump, but they still cannot achieve more fine-grained decoupling communication and still cannot meet the requirements of some services.

Actual business case

Example 1: In the merchant list (WMPageKit) on the home page of takeout, when you enter a merchant (WMRestaurantKit) and select 5 items to return to the home page, the corresponding merchant cell needs to display the selected item “5”.

Example 2: The search result (WMSearchKit) jumps to the container page of the supermarket (WMSupermarketKit), which needs to pass a general Domain (also known as Model, Model, Entity, Object, etc., which is represented by Domain uniformly below).

Example 3: To make a one-click order requirement (WMPageKit), one method that calls the order function (WMOrderKit) takes an order-related Domain and a VC and does not return a value.

These scenarios basically cover the basic functions required for component communication, so what is the most elegant solution?

Exploration of component communication

Model analysis

For the actual business case above, it is easy to think of three solutions: copying codependent code, direct dependencies, and sinking common dependencies.

For scheme 1, multiple redundant codes will be maintained and the code will not be synchronized after logical update, which is obviously not desirable. As for scheme 2, it will introduce more useless dependencies for the caller, and may cause the problem of cyclic dependencies between components, leading to the component cannot be published. As for scheme 3, it is actually a feasible solution, but the development cost is high. For components that sink out, it can be difficult to find a clear location and end up as a “hodgepodge” of dependencies, leading to serious maintenance problems.

So how do you solve this problem? According to Dependency Inversion Principle, one of the five principles of object-oriented design, high-level modules should not depend on low-level modules; both should depend on abstract interfaces. By extension to the relationship between components, we also want to avoid direct dependencies for the callers and callees between components, and instead want them to rely on a common abstraction layer that can be managed and used by architectural tools. This allows us to elegantly communicate between components while removing unnecessary dependencies during build time.

A few broad directions for existing industry solutions

There are many ways to practice the dependency inversion principle. On the iOS side, the OC language and the Foundation library provide several language tools for abstraction. In this section we will examine some of these practices.

1. Use dependency injection

There are objects and Typhoon, both of which are dependency injection frameworks in OC. The former is lightweight, while the latter is heavier and supports Swift.

Compared with general method is to use “agreements” < – > “class” binding way, for the injected object constraints will have corresponding Protocol, often can see some RegisterClass: ForProtocol: and classFromProtocol code. When an injected object is needed, an interface provided by the framework is used to get the required initialized object from the container with the protocol as an input parameter. It is also possible to Register a Block of block-code directly, which is used to initialize itself and is returned as a value of type ID. It is possible to support some compilation checks to ensure that the corresponding Code is compiled.

Some run-time loading operations are pushed forward to compile time. For example, change entries from +load to use __attribute((used,section(“__DATA,key”)) to write items to the Segment of the Mach-o file Data at compile time to reduce cold start time.

Therefore, the limitations of this scheme are: the performance consumption of code block access is high, and the maintenance of the binding relationship between protocol and class needs more time cost.

2. Based on SPI mechanism

Service Provider Interfaces stands for ServiceLoader.

The implementation process is roughly as follows: there is no dependence between A library and B library, but both rely on P platform. The type of input parameter and return value needs to be included in the platform layer. The implementation of interface I is stored in the B library (because the implementation is in the B library, so the implementation can normally reference elements of the B library). The A library then functions through this interface I of the P platform. A can call interface I, but is implemented in B’s library.

Use serviceloader. load (interface, key). Use reflection to find the imp file path through the registered key and then get the instance object to call the corresponding interface.

This operation is widely used in Android and is roughly equivalent to replacing a coupled reference like import with a reflection operation. In fact, in iOS, you don’t have to do that with reflection.

With reflection, Java can implement something similar to ClassFromString, but you can’t use MethodFromString directly. And ClassFromString is through the string map to the file path, this class is similar to com. Waimai. Home. The searchImp, thus can obtain type and then instantiated, and a reflection of OC is implemented through the message mechanism.

3. Based on notification center

In my previous communication with a student who makes a book App, I found that teams of some companies in the industry use NotificationCenter for decoupling communication. Because the NotificationCenter itself supports object transfer and the function of the NotificationCenter also supports synchronous execution, it can also achieve the purpose.

Notification Center has received a major update since iOS 9, which supports the request and Response processing logic for notifications, and supports retrieving notification senders. This is an improvement over the old days when notifications were sent out in groups without being aware of who sent them or whether they were received.

The string convention can also be understood as a simplified protocol that can be set up as macros or constants for unified maintenance at the platform level.

The obvious drawbacks are that the unified paradigm of development is hard to constrain, styles vary, and strings are still hard to manage compared to interfaces.

4. Use objc_msgSend

This is the most versatile approach to iOS native messaging and is written with some hard coding. The core code is as follows:

id s = ((id(*) (id, SEL))objc_msgSend)(ClassName,@selector(methodName)); 
Copy the code

The nature of this approach is plug-and-play, allowing functionality to be implemented quickly when the developer is 100% sure that the whole call chain is fine.

The defects of this scheme lie in the compilation of very random, check and verify the logic is not enough, full screen strong rotation. Int, Integer, NSNumber, and so on are prone to type conversion errors, although the result is not an error, but the number will have an error.

Scheme comparison

Next, let’s do some performance comparisons for these general directions.

Considering the practical usage and limitations in the company, there may be several more steps than the conventional method, and the results may be somewhat different from the conventional naked test. For example, dependency injection is usually stored in singletons (memory), but in order to optimize the cold start time, we have written into the Segment of the Mach-o file Data, so the access time will be relatively long under our statistical scope.

// In order not to expose the class name of the business attribute replaced by "some", and hide the initialization, loop 100W times, difference calculation code, the key operation code is as follows

// Access the injected object
xxConfig = [[WMSomeGlueCore sharedInstance] createObjectForProtocol:@protocol(WMSomeProtocol)];
// Notification sent
[[NSNotificationCenter defaultCenter]postNotificationName:@"nixx" object:nil];
// Native interface call
a = [WMSomeClass class];
// reflection calls
b = objc_getClass("WMSomeClass");

Copy the code

The following information is displayed:

As you can see, native interface calls are clearly the most efficient use, reflecting times are an order of magnitude longer than native ones, but 100W times is tens of milliseconds more, which is within acceptable limits. Notification sending is low in performance by comparison, and access injection objects are even lower.

Of course, in addition to performance consumption, there are many dimensions that are difficult to quantify, including specification constraints, functionality, code amount, readability, etc. The author gives the comparison score according to the objective evaluation of the actual scene.

Below, we compare the advantages and disadvantages of each scheme with the ability value graph of five dimensions:

  • The scoring of each dimension takes into account certain practical scenarios and may be slightly different from the conventional results.

  • We’ve done the transformation. The bigger the area, the better. The longer the dimension of readability, the higher the readability, and the longer the dimension of code volume, the lower the code cost.

As shown in Figure 2, you can see that each of the four approaches in the figure above has more or less drawbacks:

  1. Dependency injection is due to the actual scenarios of The United States, so there are obvious shortcomings in performance consumption, and the amount of code and readability are not outstanding, specification constraints here is the highlight.
  2. The SCOPE diagram of the SPI mechanism is large, but reflection is used, and the code development costs are high. In practice, protocol management is required.
  3. The notification center looks convenient, but send and receive mostly come in pairs, with binding methods or blocks attached, and there’s not a lot of code.
  4. Msgsend, on the other hand, is very powerful and has very little code, but is almost zero in terms of specification constraints and readability.

From a comprehensive perspective, SPI and objc_msgSend have obvious characteristics and great potential. If these two schemes are improved to a certain extent, a scheme with a higher comprehensive score can be achieved.

A scheme perfected or derived from an existing scheme

5. Use the Category + NSInvocation

This scheme evolved from objc_msgSend. Objc_msgSend is used at the bottom of the NSInvocation, but there are many types of specification issues that can be resolved with method signatures and return value type verification. There are no cumbersome registration procedures in this way, and any new interface added can be directly added in the low-level library.

To further restrict the interfaces that callers can call, create categories to provide the interfaces, internally wrap the underlying interfaces, and limit the actual types of both return values and incoming parameters. A close example in the industry is Casatwy’s CTMediator.

6. Native CategoryCoverOrigin mode

This scheme evolved from the SPI approach. What both have in common is that they provide interfaces at the platform level to be invoked by the business side, but the difference is that this approach completely avoids hard coding. And CategoryCoverOrigin is an idea without any framework code, so it can be said that OC Runtime is the framework support of this scheme. The core operation of this scheme is to summarize all business interfaces in the base class and overwrite the declared interfaces in the Category of the base class created in the upper business library. There is no hard coding or reflection.

The capabilities of the two evolving schemes are evaluated as follows (green), and a comparison with the pre-evolving scheme (orange) is pasted in the figure:

The above description of the two schemes is very general, and some students may question the ability assessment. The following will be detailed introduction, and describe the actual operation of noteworthy details. The two schemes combine to form WMScheduler, a component communication framework within takeout.

WMScheduler component communication

The WMScheduler of takeout mainly realizes inter-component communication by using the Category feature. In practice, there are two application schemes: Category+NSInvocation and Category CoverOrigin.

1. The Category + NSInvocation scheme

Program Introduction:

This solution provides a simple and versatile interface with the NSInvocation code for fault – tolerant encapsulation, parameter determination and type conversion in the lower layer. The communication scheduler class is created in the upper layer to provide common interfaces, and the special interfaces for specific services are extended in the Category of the scheduler. All the upper interfaces are governed by the specification, and the interior of the specification interface calls the lower level simple universal interface that calls any method via the NSInvocation hard-coded operations.

UML diagrams:

As shown in Figure 3-1, the core of the code is in the WMSchedulerCore class, which includes NSInvocation target and Method operation, parameter handling (including object, basic data type, NULL type), exception handling, etc. Finally, we open up a concise universal interface with parameters like target, Method, parameters, etc., and then do the internal calls for us. Instead of the interface being called directly by the upper level business, you need to create a Category of WMSchedule R in which to write the canonical interface (prefix, input parameter type, return value type are all defined).

It is worth noting that categories that provide business-specific interfaces are based not on WMSchedulerCore but on WMScheduler. Seemingly superfluous, is actually to do the isolation of authority. Upper-layer business can only access the canonical interface of wmScheduler.h and its Category. There is no access to the “versatile but non-standard” interface provided by wmSchedulerCore.h.

For example, in the UML diagram, you can see that only wms_getOrderCountWithPoiid (the canonical interface) can be called, and wm_excuteInstance Method (the universal interface) cannot be used.

To better understand the use in practice, I post the complete code for the component call cycle:

As shown in Figure 3-2, in this scheme, “B library calls A library method” only needs to change the codes of the two warehouses, and the files to be changed are underlined. Please take A closer look at the sample codes.

Sample code:

Platform (general function) library three files:

1.

// WMScheduler+AKit.h
#import "WMScheduler.h"
@interface WMScheduler(AKit)
* param poiid * @return Actual number of red dots */
+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID;
@end
Copy the code

2.

// WMScheduler+AKit.m
#import "WMSchedulerCore.h"
#import "WMScheduler+AKit.h"
#import "NSObject+WMScheduler.h"
@implementation WMScheduler (AKit)
+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID{
    if (nil == poiid) {
        return 0;
    }
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
    id singleton = [wm_scheduler_getClass("WMXXXSingleton") wm_executeMethod:@selector(sharedInstance)];
    NSNumber* orderFoodCount = [singleton wm_executeMethod:@selector(calculateOrderedFoodCountWithPoiID:) params:@[poiID]];
    return orderFoodCount == nil ? 0 : [orderFoodCount integerValue];
#pragma clang diagnostic pop
}
@end
Copy the code

3.

// WMSchedulerInterfaceList.h
#ifndef WMSchedulerInterfaceList_h
#define WMSchedulerInterfaceList_h
// This file will be added to the PCH of the upper-level business, so we do not need to import this file below
#import "WMScheduler.h"
#import "WMScheduler+AKit.h"
#endif /* WMSchedulerInterfaceList_h */
Copy the code

BKit (caller) a file:

// WMHomeVC.m
@interface WMHomeVC()"UITableViewDataSource.UITableViewDelegate>
@end
@implementation WMHomeVC.NSUInteger *foodCount = [WMScheduler wms_getOrderedFoodCountWithPoiID:currentPoi.poiID];
    NSLog(@"%ld",foodCount); .@end
Copy the code

Code analysis:

The four files above complete a cross-component invocation. Lines 30 and 31 of WMScheduler+ Akit. m call the existing AKit method because WMSchedulerCore provides NSInvocation. So I can just call it up. The interface provided by WMScheduler+AKit is the “specification interface” described above. This interface is no different when WMHomeVC (caller) calls OC methods in the repository.

Stretch your thinking:

  • In the example above, both the input and return values are basic data types, and the Domain is supported, provided that the Domain is placed in the platform library. We can divide the Domain in the project into BO (Business Object), VO (View Object) and TO (Transfer Object). VO often appears in View and cell, while BO is generally only used inside each Business sub-library. The TO is a generic model that needs TO be placed in the platform library for communication between components. For example: generic PoiDomain, generic OrderDomain, generic AddressDomain, and so on. These domains, called TO, can be used as input parameter types or return value types for the specification interface.

  • In actual business scenarios, the need to transfer Domain when jumping pages is also a commonplace problem. Most page-level jump frameworks only support the transfer of basic data types (there are trick methods to transfer Domain memory addresses but they are not elegant). With the capability supported above, we can get the VC of the target page through the universal interface in the specification interface, and call the set method of one of its attributes to assign the Domain we want to pass over, and then return the VC object as a return value. The caller gets the VC and pushes it in the current navigation stack.

  • In the above code, we use a named calculateOrderedFoodCount WMScheduler calls the Akit WithPoiID: method. So there is a debate: when component communication requires a method to be called, is it allowed to call the existing method directly, or is it a copy of the prefixed method that provides component communication? The problem of the former is that existing methods may be modified, extended parameters will directly cause callers to find methods, and Method strings will not compile errors (line 31 of the platform code WMScheduler+ akit.m above). The problem with the latter is that it greatly increases development costs. So we’re going to use the former, and we’re going to do some special stuff, so if an existing method is modified, we’re going to check it out here in isReponseForSelector, and we’re going to go to the else assertion and we’re going to find it.

Phase summary:

The advantage of the Category+NSInvocation solution is convenience, because the Category interface is in the platform library, and other callers besides BKit can call the invocation directly.

But let’s list the inelegant ones:

  • When there are many lines of code inside the cross-component method, a lot of hard coding is done.

  • The method string is hardcoded so that the compiler detects no errors when an existing method is modified (only by assertion constraints).

  • The design of the underlying library calling up will be criticized.

The CategoryCoverOrigin solution, described next, addresses these three issues.

2. CategoryCoverOrigin scheme

Program Introduction:

First of all, this has nothing to do with the NSInvocation, this is a completely different concept from the previous one, so don’t use the same mentality from the previous one here.

The idea of this scheme is to provide interface methods in wMScheduler. h of the platform layer. The implementation of the interface is only empty implementation or bottom-pocket implementation (in the bottom-pocket implementation, toast hints or assertions can be added in the Debug environment according to business scenarios). Replace the base class method with the same name at run time. The caller normally calls the interface provided by the platform layer. In CategoryCoverOrigin’s scenario, the categories of the WMScheduler are inside the provider repository, so the business logic dependencies can be made inside the repository using regular OC calls.

UML diagrams:

As you can see from Figure 4-1, the Category of WMScheduler is moved to the business repository and there is a complete set of all interfaces in WMScheduler.

To better understand CategoryCover in practice, I have posted a complete code for CategoryCover:

As shown in Figure 4-2, in this scenario, the requirement of “B library calling A library method” needs to modify the code of three repositories, but there are no other dependencies except these four edited files. Please take A closer look at the code examples.

Sample code:

Platform (general function library) two files

1.

// WMScheduler.h
@interface WMScheduler : NSObject
// This file is a summary of all component communication methods
#pragma mark - AKit  
* param poiid * @return Actual number of red dots */
+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID;
#pragma mark - CKit
// ...
#pragma mark - DKit
// ...
@end
Copy the code

2.

// WMScheduler.m
#import "WMScheduler.h"
@implementation WMScheduler
#pragma mark - Akit
+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID
{
		return 0; // This.m only requires an empty implementation as a bottom-pocket solution.
}
#pragma mark - Ckit
// ...
#pragma mark - Dkit
// ...
@end
Copy the code

AKit (provider) a Category file:

// WMScheduler+AKit.m
#import "WMScheduler.h"
#import "WMAKitBusinessManager.h"
#import "WMXXXSingleton.h"  
// Import a lot of akIT-related business files directly, because they are in AKit warehouse
@implementation WMScheduler (AKit)
// This macro can mask warnings about classes overwriting base class methods
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
// The method written in the platform layer is auto-complete
+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID
{
  	if (nil == poiid) {
        return 0;
    }
  	// All akIT-related classes can be directly interface called, no hard coding is required, can be compared to the previous way.
    WMXXXSingleton *singleton = [WMXXXSingleton sharedInstance];
    NSNumber *orderFoodCount = [singleton calculateOrderedFoodCountWithPoiID:poiID];
    return orderFoodCount == nil ? 0 : [orderFoodCount integerValue];
}
#pragma clang diagnostic pop
@end
Copy the code

BKit (caller) a file written unchanged:

// WMHomeVC.m
@interface WMHomeVC()"UITableViewDataSource.UITableViewDelegate>
@end
@implementation WMHomeVC.NSUInteger *foodCount = [WMScheduler wms_getOrderedFoodCountWithPoiID:currentPoi.poiID];
    NSLog(@"%ld",foodCount); .@end
Copy the code

Code analysis:

By CategoryCoverOrigin, the platform library stores a summary of all component communication interfaces in a wmScheduler.h file, separated by comments, with empty implementations written in a.m file. The functional code is written in WMScheduler+AKit. M of the service provider’s repository. See lines 17 and 18 of this file for the business logic to be called using the regular OC interface. At runtime, methods of this Category override methods of the same name in the wmScheduler.h base class to do this. The CategoryCoverOrigin method does not require the support of other feature classes.

Stretch your thinking:

If there are many business libraries and many methods, will there be a WmScheduler.h explosion? At present, the actual scenarios of cross-component invocation of our project are not many, so they are summarized in a file. If the screen is full of cross-component invocation projects, we need to consider whether the business architecture and module division are reasonable. Of course, if wmScheduler. h does explode, it is perfectly possible to move the interface of each business to its own Category. H file and create a WMSchedulerInterfaceList file to import these categories.

A choice between two options

The pros and cons of Category+NSInvocation and CategoryCoverOrigin are summarized here.

Category+NSInvocation CategoryCover
advantages There is less time cost in the process to change only two warehouses

Url invocation methods can be implemented

(scheme: / / target/method:? Para = x)
disadvantages The cost of hard coding is high when the function is complex

When upper-layer services change, the platform interface is affected

I would prefer to use CategoryCoverOrigin’s non-hard coding solution, depending on the actual scenario of the project to make the best choice.

For more advice

  • As for the interfaces provided by components, we prefer to borrow ideas from SPI. What functions should be exposed as a Kit? What services are provided for other parties to decouple calls? It is recommended to proactively open up the core method to minimize the “use only” scenario. For example, the global shopping cart needs to “provide a way to get the number of red dots”, and the merchant center needs to provide an interface service to “get the Domain of the entire Poi based on the string ID”.

  • You need to take abstraction into account and provide a more generic interface. For example, the method of “getting the lowest full reduction price and stitching it into a text to return a string” is not generality of the method of “getting the lowest full reduction price”.

Category Risk management and control

Let me start with two cases that have happened

1. A question about repeated coverage of NSDate in October 2017

At that time, Meituan platform had NSDate+MTAddition class, and in the takeout side had NSDate+WMAddition class. The former NSDate+MTAddition has the method getCurrentTimestamp, which returns a timestamp of seconds. The latter NSDate+WMAddition also adds the getCurrentTimestamp method to the one-time requirement, but uses milliseconds to return values consistent with other platforms. The takeout analogies to platform classes are later in the normal loading order, so no problems were found in the takeout tests. However, after it was integrated into the main project of Imeituan, the method that returned “second” called by other business parties was overwritten by the method with the same name that returned “millisecond” in the delivery test, resulting in problems such as interface error and UI disorder.

2. Communication problems encountered by a WMScheduler component in March 2018

There are order components and merchant container components in the takeaway side, the connection between the two components is very close, some functions in either of the two warehouses are said to make sense. Hence the scenario where two warehouses write methods with the same name. In the Restaurant and WMScheduler WMScheduler + + Order both warehouse added method – (void) wms_enterGlobalCartPageFromPage:, in running the two one is covered. In a Bug fix, exception handling code was added to one of the places, which happened to be loaded first, but was overwritten by the method of the same name loaded later. This caused the exception handling code to not take effect.

Is the way CategoryCover is used unsafe? NO! As long as the rules are clear, the risk points can be completely controlled. Next, let’s analyze the coverage principle of categories.

Category method coverage principle

  1. The Category method does not “completely replace” the existing methods of the original class. That is, if both the Category and the original class have methodA, then the list of methods of the class will have two methodA after the Category is appended.

  2. The Category method is placed in front of the new method list, and the method of the original class is placed behind the new method list, and that’s what we call a Category method that overwrites the method of the original class with the same name, and that’s because when we run it, we look up the methods in the order of the method list, As long as it finds a method corresponding to the name, it will stop ^_^, but there may be another method with the same name.

Categories are decided at runtime, whereas base classes are decided at compile time, so the loading order of the methods in the classification must come after the base class.

There was a technical blog on Meituan that took an in-depth look at categories and broke down classification overwriting from a compiler and source point of view: An in-depth understanding of Objective-C: categories

According to the principle of method coverage, we can analyze which operations are safe and which have risks, and manage them accordingly. Next, we will introduce some work related to meituan Category management.

Category method management

Due to historical reasons, no matter what kind of management rules, can not be directly “one size fits all”. Therefore, in view of the current situation, we split the whole management process into three parts: “data”, “scene” and “strategy”.

The data layer is responsible for finding abnormal data, and all policies share one data layer. For the data acquisition of Category method, we have the following methods:

Based on the analysis of advantages and disadvantages, and considering that Meituan has completely realized the “componentization” project, it is better to carry out the management and control of Category after the integration stage. We finally chose to use Linkmap for data acquisition, and the specific methods will be introduced in the following sections.

In the strategy part, exceptions are controlled for different scenarios. The main development work is on our componentized CI system, namely the Hyperloop system introduced earlier.

Hyperloop itself provides a number of policy functions, including whitelist, publishing integrated process management, and so on. We just need to develop the tools in association. The data layer we developed as a standalone component also ends up running on Hyperloop.

The strategy of segmentation based on scenarios is shown in the table below (it should be noted that some scenarios in the table do not actually exist, but are listed strictly for thinking) :

The governance of CategoryCoverOrigin’s component communication scheme that we described earlier is embodied in point 2. The control of the two cases mentioned in risk control is mainly reflected in point 4.

Principle of Category data acquisition

In the previous chapter, we mentioned the method of linkmap analysis for Category data acquisition. In this chapter, we describe the practice in detail.

Enable linkmap

First of all, linkmap generation is turned off by default, we need to manually turn on the switch and configure the storage path in build Settings. For Meituan project and Meituan Takeout project, we will use the internal meituan cloud storage tool for persistent storage of linkmap generated after each formal construction to ensure subsequent traceability.

Linkmap composition

To parse a Linkmap, you first need to understand the composition of a Linkmap.

As the name indicates, the Linkmap file is generated after the code link and mainly consists of four parts: basic information, Object Files table, Sections table and Symbols table.

The first two lines are the basic information, including the binary path to link completion and the schema. If there are multiple end products in a project (such as Watch App or Extension), each architecture of each product will generate a Linkmap after configuration.

# Path: /var/folders/tk/xmlx38_x605127f0fhhp_n1r0000gn/T/d20180828-59923-v4pjhg/output-sandbox/DerivedData/Build/Intermediates.n oindex/ArchiveIntermediates/imeituan/InstallationBuildProductsLocation/Applications/imeituan.app/imeituan
# Arch: arm64
Copy the code

The Object Files of the second part lists all Object files used for linking, including those compiled by the code, those in the static link library and those in the dynamic link library (such as the system library), and assigns a file ID to each Object file.

# Object files:[ 0] linker synthesized [ 1] dtrace [ 2] /var/folders/tk/xmlx38_x605127f0fhhp_n1r0000gn/T/d20180828-59923-v4pjhg/output-sandbox/DerivedData/Build/Intermediates.n oindex/ArchiveIntermediates/imeituan/IntermediateBuildFilesPath/imeituan.build/DailyBuild-iphoneos/imeituan.build/Object S - normal/arm64 / main. O... [26] /private/var/folders/tk/xmlx38_x605127f0fhhp_n1r0000gn/T/d20180828-59923-v4pjhg/repo-sandbox/imeituan/Pods/AFNetworking/ Bin/libAFNetworking. A (AFHTTPRequestOperation. O)... [25919] / Applications/Xcode. App/Contents/Developer/Platforms/iPhoneOS platform/Developer/SDKs/iPhoneOS11.3 SDK/usr/lib/libobjc. T bd [25920] / Applications/Xcode. App/Contents/Developer/Platforms/iPhoneOS platform/Developer/SDKs/iPhoneOS11.3 SDK/usr/lib/libSystem .tbdCopy the code

The third Section, Sections, records all Sections and their segments and sizes.

# Sections:
# Address Size Segment Section0x100004450 0x07A8A8D0 __TEXT __TEXT...... 0x109EA52C0 0x002580A0 __DATA __objc_data 0x10A0FD360 0x001D8570 __DATA __data 0x10A2D58D0 0x0000B960 __DATA __objc_k_kylin... 0x10BFE4E5D 0x004CBE63 __RODATA __objc_methname 0x10C4B0CC0 0x000D560B __RODATA __objc_classnameCopy the code

The fourth part “Symbols” is the highlight, which lists the information of all Symbols, including their object file and size. In addition to OC methods, class names, protocol names, etc., symbols also contain blocks, literal strings, etc., which can be used for other requirements analysis.

# Symbols:
# Address Size File Name0x1000045B8 0x00000060 [ 2] ___llvm_gcov_writeout 0x100004618 0x00000028 [ 2] ___llvm_gcov_flush 0x100004640 0x00000014 [ 2] ___llvm_gcov_init 0x100004654 0x00000014 [ 2] ___llvm_gcov_init.4 0x100004668 0x00000014 [ 2] ___llvm_gcov_init.6 0x10000467C 0x0000015C [3] _main...... 0x10002F56C 0x00000028 [ 38] -[UIButton(_AFNetworking) af_imageRequestOperationForState:] 0x10002F594 0x0000002C [ 38] -[UIButton(_AFNetworking) af_setImageRequestOperation:forState:]
0x10002F5C0	0x00000028	[ 38] -[UIButton(_AFNetworking) af_backgroundImageRequestOperationForState:]
0x10002F5E8	0x0000002C	[ 38] -[UIButton(_AFNetworking) af_setBackgroundImageRequestOperation:forState:]
0x10002F614	0x0000006C	[ 38] +[UIButton(AFNetworking) sharedImageCache]
0x10002F680	0x00000010	[ 38] +[UIButton(AFNetworking) setSharedImageCache:] 0x10002F690 0x00000084 [38] -[UIButton(AFNetworking) imageResponseSerializer]......Copy the code

Linkmap digital

According to the above analysis, after understanding the format of Linkmap, data can be extracted through simple text analysis. Because the internal iOS development tool chain of Meituan uses Ruby, linkMap analysis is also developed in Ruby, and the whole parser is packaged into a Ruby Gem.

In terms of implementation, considering universality, our LinkMap parsing tool is divided into three layers: parsing, model and parser, and each layer can be independently extended.

For Category parsers, the Link Map Parser parses the specified LinkMap to generate instances of the generic model. Get the Symbol class from the instance, filter out the symbol with “()” in the name, that is, the Category method.

Then you just aggregate by method name, and if there’s more than one, you’re bound to have Category method conflicts. Based on the scenario described in the previous section, analyze the specific conflict types and provide a conclusion for the Hyperloop.

You can refer directly to our tool test case for the external interface. Eventually the Gem will be used directly by the Hyperloop.

 it 'should return a map with keys for method name and classify' do
    @parser = LinkmapParser::Parser.new
    @file_path = 'spec/fixtures/imeituan-LinkMap-normal-arm64.txt'
    @analyze_result_with_classification = @parser.parse @file_path

    expect(@analyze_result_with_classification.class).to eq(Hash)

    # Category methods conflict
    symbol = @analyze_result_with_classification["-[NSDate isEqualToDateDay:]"]
    expect(symbol.class).to eq(Hash)
    expect(symbol[:type]).to eq([LinkmapParser::CategoryConflictType::CONFLICT])
    expect(symbol[:detail].class).to eq(Array)
    expect(symbol[:detail].count).to eq(3)

    The Category method overrides the original method
    symbol = @analyze_result_with_classification["-[UGCReviewManager setCommonConfig:]"]
    expect(symbol.class).to eq(Hash)
    expect(symbol[:type]).to eq([LinkmapParser::CategoryConflictType::REPLACE])
    expect(symbol[:detail].class).to eq(Array)
    expect(symbol[:detail].count).to eq(2)
  end
Copy the code

The Category method manages summaries

1. Risk management

For any grammatical tool, there are pros and cons. So in addition to exploring their real-world applications, it’s important to always be aware of the risks they pose and choose the right tools and timing to manage those risks.

Xcode itself provides a number of tools and opportunities to analyze the build process and artifacts. If you encounter a few hicks in your daily work, think of management in terms of build-phase tools. For example, Linkmap mentioned in this article can be used not only for Category analysis, but also for binary size analysis, component information management, and so on. Investing some resources in the development of related tools can often get twice the result with half the effort.

Code specification

Going back to the use of categories, in addition to tool controls, we also have code specifications that manage risk at source. For example, in the specification we require all Category methods to use prefixes to reduce the possibility of unintentional conflicts. And we also plan to make “using prefixes” one of our controls.

3. Follow-up planning

1. Check of overwriting system method Since the system symbol table has not been introduced into the current control system, it is impossible to analyze and intercept the behavior of overwriting system method. We plan to open up the symbol table system with Crash analysis system in the future to find improper coverage of the system library in advance.

2. Tool reuse The current control system only applies to Meituan takeout and Meituan App, and plans to promote it to other apps in the future. With Hyperloop, things are not technically too difficult. From the tool’s point of view, we have plans to open source the data layer code at the right time, hopefully helping with more development.

conclusion

In this article, we start with specific business scenarios, summarize the general model of inter-component invocation, and analyze and compare common decoupling schemes, and finally choose the most suitable scheme for our business scenarios at present. In other words, dependency inversion is achieved through Category coverage, and the build-time dependency is deferred to runtime, achieving the desired decoupling goal. At the same time, potential problems of the solution are avoided through linkmap tool management and control.

In addition, as we mentioned in the model design, component decoupling actually has many options on the iOS side. We will continue to share other solutions and practices with you. We hope that our work can shed some light on the decoupling of iOS development components.

Author’s brief introduction

Shang Xian, senior engineer of Meituan. I joined Meituan in 2015 and now serve as the leader of virtual team of Meituan Takeout iOS platform, mainly responsible for business architecture, continuous integration and engineering. At the same time, HE is also a lover of new technologies in the field of mobile terminal. He is responsible for solving many difficulties in the implementation of new technologies in the takeout business. Currently, he owns seven national invention patents.

Ze Xiang, technical expert of Meituan, joined Meituan in 2014 and has been responsible for the construction of iOS continuous integration system, iOS terminal platform business and basic business of Meituan iOS terminal. Currently, as the Team Leader of Meituan Mobile platform architecture platform group, I am mainly responsible for meituan App platform architecture, componentization, R&D process optimization and partial infrastructure construction, and am committed to improving the r&d efficiency and quality of all businesses on the platform.

Recruitment information

Meituan takeaways is looking for senior/senior engineers and technical experts in iOS, Android and FE based in Beijing, Shanghai and Chengdu. Please send your resume to [email protected].