takeaway

Technical products from Sohu:

This article is based on a real case encountered in the project, from a specific example of UITableView implementation, trying to explore the implementation details of iOS system framework through SIL (Swift Intermediate Language), the Intermediate Language.

In the process, we also discussed what the Thunk function is, the Swift messaging method, and verified our conclusions and assumptions by constantly modifying the code and conducting comparison tests, hoping to spark more thinking.

First, the question is elicited

Let’s take a look at the following code, which is a common tableView usage scenario:

protocol ListDataProtocol {}class BaseViewController<P: ListDataProtocol>:UIViewController,UITableViewDelegate,UITableViewDataSource { var tableView: UITableView var presenter: P? Override func viewDidLoad() {// omit unnecessary implementation details} override init(nibName nibNameOrNil: String? , bundle nibBundleOrNil: Bundle?) {// omit non-required implementation details} required init? (coder: NSCoder) {// omit unnecessary details} //MARK: UITableViewDataSource // Number of cells func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 10 } // UITableViewCell func tableView(_ tableView: UITableView, cellForRowAt indexPath: indexPath) -> UITableViewCell {// omit unnecessary implementation details} UITableViewDelegate // Set cell height func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {return 44.0}}class ListData: NSObject, ListDataProtocol{} Class ViewController: BaseViewController<ListData> { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after Func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { print(indexPath.row) }}Copy the code

The base class BaseViewController contains the basic implementation of the DataSource and Delegate of the iOS UITableView. The subclass ViewController inherits the base class. Since the base class does not know how to handle specific business details, So only the required proxy implementation is included, and the alternative proxy implementation is expected to be implemented by subclasses, so table(_:didSelectRowAt:) is implemented in the subclass ViewController to handle Cell selection events.

The question, then, is whether clicking on the cell when executing this code will output as expectedprint(indexPath.row)?

**** has no output, ****table(_:didSelectRowAt:)** will not be called to execute, so why?

As the specific implementation of Fundation framework of the system was involved, neither official documents nor Google could find the answer, so I thought of using SIL (Swift Intermediate Language), the Intermediate Language provided by Swift, to try to find some hints of the underlying implementation. Help us understand.

If you do not know SIL, it is recommended that you first read Preliminary To Swift Intermediate Language.

Generate SIL

Swiftc-emit - Silgen -target x86_64-apple-ios13.0 simulator - SDK $(xcrun -- show-sdK-path -- SDK iphonesimulator) -onOne test.swift > test.swift.silCopy the code
  • Since we need to use system libraries such as UIKit, we need to specify the SDK path to rely on using the -sdk parameter.

  • -target Specifies the target of the generated code.

  • -Onone does not perform any optimizations.

SIL analysis

Thunk function

Thunk function ①, is a concept that has to be mentioned, I borrow a paragraph from Ruan Yifeng teacher’s blog ② to illustrate:

When a programming language is in its infancy, the compiler writes well. One bone of contention is the “evaluation strategy “**③, that is, when exactly the parameters of a function should be evaluated.

var x = 1; function f(m){ return m * 2; }f(x + 5)Copy the code

One suggestion is that **” call by value “**④ (call by value), which computs the value of x + 5 (equal to 6) before entering the function body, and passes the value to f. C uses this strategy.

F (x + 5)// When called, equivalent to f(6)Copy the code

Another suggestion is to pass the expression x + 5 directly into the function body and evaluate it only when it is used. The Hskell language uses this strategy.

F (x + 5)// is the same as (x + 5) * 2Copy the code

Which is better, a pass-value call or a pass-name call? The answer is both pros and cons. The pass-value call is relatively simple, but the parameter is not actually used when evaluating it, which can cause a performance penalty.

The compiler’s “call by name” implementation usually puts arguments in a temporary function that is passed into the function body. This temporary function is called the Thunk function.

Swift SIL thunk function of the basic thoughts and described above is consistent, but slightly different, let’s take a look at this example, headed with BaseViewController. The corresponding segments of SIL viewDidLoad function, to explore the effects of thunk function.

// BaseViewController.viewDidLoad()sil hidden [ossa] @$s4test18BaseViewControllerC11viewDidLoadyyF : $@convention(method) <P where P : ListDataProtocol> (@guaranteed BaseViewController<P>) -> () {// %0 // users: %89, %88, %87, %80, %79, %78, %72, %71, %53, %11, %10, %2, %1bb0(%0 : @guaranteed $BaseViewController<P>):// Preds: // users: %77, %75, %74bb2(%70: @owned $UIView): // Preds: Bb0 / / has nothing to do temporarily omitted code} / / end sil function '$s4test18BaseViewControllerC11viewDidLoadyyF / / @ objc BaseViewController.viewDidLoad()sil hidden [thunk] [ossa] @$s4test18BaseViewControllerC11viewDidLoadyyFTo : $@convention(objc_method) <P where P : ListDataProtocol> (BaseViewController<P>) -> () {// %0 // user: %1bb0(%0 : @ $BaseViewController unowned < P >) : / / has nothing to do temporarily omitted code / / function_ref BaseViewController. ViewDidLoad () % 3 = function_ref @ $s4test18BaseViewControllerC11viewDidLoadyyF: $@ convention (method) < tau _0_0 where tau _0_0: ListDataProtocol> (@guaranteed BaseViewController<τ_0_0>) -> () // user: %4 = apply %3<P>(%2) : $@convention(method) <τ_0_0 where τ_0_0: ListDataProtocol> (@guaranteed BaseViewController<τ_0_0>) -> () % 7 / / has nothing to do temporarily omitted code / / id: % 7} / / end sil function '$s4test18BaseViewControllerC11viewDidLoadyyFTo'Copy the code

Due to space constraints, this code omitted some unnecessary code to explain the problem, if you want to see the complete code please refer to the download address at the end of the article.

The first is a native Swift function called and processed using convention(method). It has three blocks inside it that do some specific things: bb0 allocates and initializes some variables; Bb1 Complete code diagnosis related work; Bb2 completes the TableView Delegate and DataSource Settings in BaseViewController, and these three parts make up the complete viewDidLoad function.

The second section, it is a thunk function, it is the identity of the id * * @ $$s4test18BaseViewControllerC11viewDidLoadyyFTo * *, Comparing the first period of id is * *, * * @ $s4test18BaseViewControllerC11viewDidLoadyyF second paragraph id is on the basis of the first paragraph added two words To, and through the @ convention (objc_method) as you can see, It is called using ObjC; Its internal uses Swift native to call the first function;

The whole process is shown in the figure below:

To recap, a new thunk function is generated in SIL that is exposed to ObjC for interaction, and the real native Swift function is called inside the thunk function. That is, if a function needs to be visible to ObjC, it needs to be wrapped as a thunk function that can be called ObjC’s way.

Swift message sending mode

To put it simply, Swift has two ways of sending messages:

  • Static dispatch, when executed, skips directly to the implementation of the method. Static dispatch can be inline and other compile-time optimizations. Static dispatch is always used for value types because there is no possibility of inheritance variability.

  • Dynamic dispatch: During the execution of dynamic dispatch, it finds the execution body of the method in the form of table according to the Runtime, and then executes it. Dynamic distribution does not have the compile-time optimization that static distribution does.

The third method of dispatching, objective-C dispatching, is essentially the thunk function mechanism we mentioned earlier, which involves the @objc Swift keyword and dynamic: @objc means that your Swift code, like classes, methods, properties, will be visible to Objective-C; Dynamic means if you do it dynamically in Objective-C;

However, these two parameters are not used separately. If dynamic is used, you must add @objc, but you can use @objc separately to make the meaning of each keyword clearer. Functions generated using @objc and dynamic, distributed in Objective-C, do not appear in the SIL VTable; The functions added @objc separately will appear in the VTable table and be distributed dynamically by Swift.

Another thing to mention is @objc visibility. Before Swift4, methods in classes inherited from NSObject were automatically exposed to Objective-C visibility by the compiler, but after Swift4, @objc had to be manually added to make it clear that the compiler no longer inferred. Reflection to SIL is also consistent, and is only visible to Objective-C if the generated function is marked @objc.

If you are not familiar with VTable and Witness Table, you are advised to read Preliminary Study of Swift Intermediate Language.

Fourth **, search for answers **

With that in mind, let’s explore the answer by focusing on the **table(_:didSelectRowAt:)** keyword

Sil_vtable BaseViewController {/ / has nothing to do temporarily omitted code # BaseViewController. TableView! 1: <P where P : ListDataProtocol> (BaseViewController<P>) -> (UITableView, Int) -> Int : @$s4test18BaseViewControllerC05tableC0_21numberOfRowsInSectionSiSo07UITableC0C_SitF // BaseViewController.tableView(_:numberOfRowsInSection:) #BaseViewController.tableView! 1: <P where P : ListDataProtocol> (BaseViewController<P>) -> (UITableView, IndexPath) -> UITableViewCell : @$s4test18BaseViewControllerC05tableC0_12cellForRowAtSo07UITableC4CellCSo0jC0C_10Foundation9IndexPathVtF // BaseViewController.tableView(_:cellForRowAt:) #BaseViewController.tableView! 1: <P where P : ListDataProtocol> (BaseViewController<P>) -> (UITableView, IndexPath) -> CGFloat : @$s4test18BaseViewControllerC05tableC0_14heightForRowAt12CoreGraphics7CGFloatVSo07UITableC0C_10Foundation9IndexPathVtF // BaseViewController.tableView(_:heightForRowAt:) #BaseViewController.deinit! deallocator.1: @ $s4test18BaseViewControllerCfD / / BaseViewController __deallocating_deinit} sil_vtable ViewController {/ / omit irrelevant code for the time being #BaseViewController.tableView! 1: <P where P : ListDataProtocol> (BaseViewController<P>) -> (UITableView, Int) -> Int : @$s4test18BaseViewControllerC05tableC0_21numberOfRowsInSectionSiSo07UITableC0C_SitF [inherited] // BaseViewController.tableView(_:numberOfRowsInSection:) #BaseViewController.tableView! 1: <P where P : ListDataProtocol> (BaseViewController<P>) -> (UITableView, IndexPath) -> UITableViewCell : @$s4test18BaseViewControllerC05tableC0_12cellForRowAtSo07UITableC4CellCSo0jC0C_10Foundation9IndexPathVtF [inherited] // BaseViewController.tableView(_:cellForRowAt:) #BaseViewController.tableView! 1: <P where P : ListDataProtocol> (BaseViewController<P>) -> (UITableView, IndexPath) -> CGFloat : @$s4test18BaseViewControllerC05tableC0_14heightForRowAt12CoreGraphics7CGFloatVSo07UITableC0C_10Foundation9IndexPathVtF [inherited] // BaseViewController.tableView(_:heightForRowAt:) #ViewController.tableView! 1: (ViewController) -> (UITableView, IndexPath) -> () : @$s4test14ViewControllerC05tableB0_14didSelectRowAtySo07UITableB0C_10Foundation9IndexPathVtF // ViewController.tableView(_:didSelectRowAt:) #ViewController.deinit! deallocator.1: @$s4test14ViewControllerCfD // ViewController.__deallocating_deinit}Copy the code

Since table(_:didSelectRowAt:) is not implemented in the base BaseViewController, it is normal that there is no corresponding function signature in the base sil_vtable; The subclass ViewController has an implementation, so table(_:didSelectRowAt:) appears, but note that it is not a thunk function.

For example, as shown below:

Look again at the SIL implementation part of **table(_:didSelectRowAt:)**

// ViewController.tableView(_:didSelectRowAt:)sil hidden [ossa] @$s4test14ViewControllerC05tableB0_14didSelectRowAtySo07UITableB0C_10Foundation9IndexPathVtF : $@convention(method) (@guaranteed UITableView, @in_guaranteed IndexPath, @guaranteed ViewController) -> () {// %0 // user: %3// %1 // users: %13, %4// %2 // user: %5bb0(%0 : @guaranteed $UITableView, %1 : $*IndexPath, %2 : @guaranteed $ViewController): } // end sil function '$s4test14ViewControllerC05tableB0_14didSelectRowAtySo07UITableB0C_10Foundation9IndexPathVtF'Copy the code

There is no @objc tag, there is no thunk tag, so the ViewController implements table(_:didSelectRowAt:), but this function is a pure native Swift function in the generated SIL. The compiler didn’t help it generate the corresponding Objective-C visible Thunk function, so it can’t be used by UITabview callbacks, so we’ve answered the initial question.

** So why doesn’t the compiler generate it? ** When does the compiler generate the thunk function?

** v. ** comparative test

Let’s start with two comparative tests:

Test1

**print(indexpath.row)** print(indexpath.row)** print(indexpath.row)**

Again, we use the SIL language to analyze the SIL generated after removing the preceding content:

  • **table(_:didSelectRowAt:)** same as current;

  • In addition to the original **table(_:didSelectRowAt:)** implementation, generates a thunk function with the @objc tag;

  • BaseViewController and ViewController are marked directly as @objc (there are no generics, they are uiKit-based and depend on Objective-C Runtime, so they can be inferred objective-C visibility by default).

Test2

Table (_:didSelectRowAt:); override (_:didSelectRowAt:); **print(indexPath. Row)** Normal output

View the modified SIL generated:

  • BaseViewController and subclass ViewController sil_vtable (_:didSelectRowAt:);

  • In addition to the original **table(_:didSelectRowAt:)** implementation, a thunk function with the @objc tag is generated;

  • BaseViewController and ViewController are not tagged @objc.

Thunk function is generated, so the result can be output normally! Different, there is no generics case, the compiler defaults to infer that Objc visibility is generated; In the case of generics, the compiler will infer thunk functions only if the base class implements the corresponding method.

Six, the summary

Check the SIL official documentation for the following paragraph:

If a derived class conforms to a protocol through inheritance from its base class, this is represented by an inherited protocol conformance, which simply references the protocol conformance for the base class.

A brief translation:

When derived classes comply with protocols by inheriting from their base class, this is called inheritance Protocol Conformance, which simply refers to the protocol implementation of the base class.

When designing the compiler in this scenario, it will simply refer to the base class implementation, so the base class must have table(_:didSelectRowAt:), and the subclass **table(_:didSelectRowAt:)** to be visible!

Seven, guess

At the end of this article, I would like to pose another question, we dare to guess why the compiler is designed to simply reference the base class protocol implementation, guess is compilation efficiency considerations!

In the case of Tableview, its DataSource and Delegate are mostly optional implementations, and if the compiler doesn’t follow current logic, it’s likely that it will need to layer through all the subclasses to find all the possible proxy implementations and turn them into thunk functions. Achieving the desired effect at the beginning of this article is obviously a very time-consuming process, and doing so is like swift adjusting @objc’s inference, making decisions for the user.

If there are mistakes welcome criticism and correction!

All source files can be downloaded from Github