Bo2SS

Bo2SS

1 Basic Course (Part 1)

This series "An Overview of the iOS Knowledge System" shares the author's learning experience in the Geek Time column "iOS Development Expert Course". The author, "Dai Ming", divides the column into four sections: 1) Basic, 2) Application, 3) Principle, 4) Native and Front-end Dance.

I have also organized my own study notes according to the author's division. At this stage, I tend to absorb the essence of the column and gradually increase my output in the later stage to witness my own growth.

Today, I will share the first section: Basic.

00 | Introduction#

In 2007, Steve Jobs released the first generation iPhone, which redefined people's perception of mobile phones and marked the beginning of the mobile internet era.

In July 2008, at the WWDC Apple Worldwide Developers Conference, Apple announced the official opening of the App Store, marking the true beginning of the mobile internet era for developers.

Instead of looking for the next hot spot in the mobile end, it is better to take the time to digest the key technologies left by the waves of the past few years, and then understand various "new technologies" on this basis, which will undoubtedly make you more proficient.

The author, Dai Ming, loves to share and likes to share his learning and work experience on Dai Ming's blog. He also shares some technical summaries through code on Dai Ming's GitHub.

01 | Establishing Your Own iOS Development Knowledge System#

For the iOS knowledge system, the author divides it into four sections: Basic, Principle, Application Development, and Native and Front-end. Here is a mind map:

Mind Map

In general, instead of learning whatever you see, you should learn with a purpose and in a systematic way to achieve better results and be able to cope with the updates and iterations of technology.

02 | How to Optimize and Monitor App Launch Speed?#

App Launch Process#

In general, the launch of an app can be divided into cold start and warm start.

  • Cold Start: A complete start process. Before clicking to start the app, its process is not in the system and a new process needs to be created for it to start.
  • Warm Start: The process of opening the app on the surface. The app's process is still in the system, and the user restarts and enters the app.

The perceived slow start by users occurs on the main thread. The cold start of an app mainly includes three stages:

  1. Before the execution of the main() function;
  2. After the execution of the main() function;
  3. After the completion of the first screen rendering.

Before the Execution of the main() Function#

Main Processes:

  • Loading the executable file, which is a collection of .o files of the app;
  • Loading dynamic libraries, performing rebase pointer adjustment and symbol binding;
  • Initial processing of the Objc runtime, including registration of Objc-related classes, category registration, uniqueness check of selectors, etc.;
  • Initialization, including executing the +load() method, calling functions decorated with attribute((constructor)), creating C++ static global variables.

Optimization Points:

  • Reduce the loading of dynamic libraries (Apple recommends using a maximum of 6 non-system dynamic libraries);
  • Reduce the loading of classes or methods that will not be used after startup;
  • The content in the +load() method can be executed after the first screen rendering is completed, or replaced with the +initialize() method (performing runtime method replacement operations in a +load() method will consume 4 milliseconds, and the cumulative effect will have a greater impact on startup speed);
  • Control the number of C++ global variables.

After the Execution of the main() Function#

Main Processes: From the start of the main() function to the completion of the execution of the relevant methods related to the first screen rendering in the appDelegate's didFinishLaunchingWithOptions method. For example, the business code of the home page is executed in this stage, including the reading and writing of configuration files required for the initial screen, the reading of large amounts of data for the initial screen list, and a large number of calculations for the initial screen rendering.

Optimization Approach: Functionally sort out which initialization functions are necessary for the first screen rendering, which initialization functions are necessary for app startup, and which initialization functions are only needed when the corresponding functions are used.

After the Completion of the First Screen Rendering#

Main Processes: All methods executed after the completion of the first screen rendering within the scope of the didFinishLaunchingWithOptions method. It mainly performs initialization of non-first screen business service modules, registration of listeners, reading of configuration files, etc.

Optimization Points: Prioritize methods that may block the main thread, otherwise it will affect the subsequent interactive operations of the user.

After understanding the work that needs to be done during the app launch phase, you can proceed to optimize the startup at the function level and method level.

Function-Level Startup Optimization#

Simply put, during the period from the start of the main() function to the completion of the first screen rendering, only handle the business related to the first screen, and perform initialization, listener registration, configuration file reading, etc. for other non-first screen business after the completion of the first screen rendering.

Method-Level Startup Optimization#

Check which time-consuming methods are on the main thread before the completion of the first screen rendering, and postpone or execute them asynchronously if they are unnecessary.

How to monitor method execution time? There are two main methods:

  1. Periodically capture the method call stack on the main thread and calculate the time consumption of each method within a certain period of time. Xcode's Time Profiler, which is included in the Xcode tool suite, uses this method. It has the characteristic of low precision, but it is sufficient.

  2. Hook the objc_msgSend method to grasp the execution time of all methods. The characteristic of this method is that it is very accurate, but it can only be used for Objective-C methods (for C methods and blocks, libffi's ffi_call can be used for hooking, but the threshold for writing and maintaining related tools is higher).

For the complete code for monitoring method execution time based on the second method, see GCDFetchFeed (open source project). Its usage is as follows:

Call [SMCallTrace start] where you need to detect time-consuming operations, and call stop and save when finished to print the call hierarchy and execution time of the methods. You can also set the maximum depth and minimum time consumption for filtering unnecessary information.

Additional information about objc_msgSend:

  • Source code can be found on Apple's open source website;
  • It is the necessary path for method execution in Objective-C, and by hooking it, all Objective-C methods can be hooked;
  • Execution logic: first obtain the information of the object's corresponding class, then obtain the method cache, find the function pointer based on the method's selector, perform exception handling, and finally jump to the implementation of the corresponding function.
  • Why it is written in assembly language: 1) It has the highest call frequency, and performance optimization on it can improve the performance of the entire app's lifecycle. Assembly language belongs to atomic-level optimization in terms of performance optimization, and it can optimize to the extreme; 2) Other languages are difficult to achieve the functionality of jumping from unknown parameters to any function pointer;
  • How to hook it can refer to Facebook's open source library fishhook - it achieves dynamic rebinding of symbols in Mach-O binary files running on iOS.

References:

Introduction to Assembly Language - Ruanyifeng

03 | Introduction to Auto Layout#

Cassowary is the layout algorithm used by Auto Layout.

When using Auto Layout, it is important to pay attention to the use of Compression Resistance Priority and Hugging Priority, which can make the layout more flexible, reduce code, and make it easier to maintain. You can refer to the Auto Layout-related Demo.

After the emergence of advanced responsive layout concepts like Flexbox in the front-end, Apple has encapsulated a UIStackView similar to Flexbox based on Auto Layout to improve the usability of responsive layout in iOS development.

PS: Currently, most projects use third-party libraries based on Auto Layout, such as Masonry - related blog.

04 | When the Project Gets Bigger and the Team Gets Larger, How to Design a More Reasonable Architecture?#

Goal: Completely decouple the business, sink common functions, make each business a separate Git repository, and generate a Pod library for each business, and then integrate them together.

In the evolution from a simple architecture to a large-scale project architecture, three problems need to be solved:

  1. How to divide the module granularity? For iOS development, which follows the object-oriented programming paradigm, we should first adhere to the SOLID principles.
  2. How to layer the architecture? It is recommended not to exceed three layers: the bottom layer can be unrelated basic components, such as network and storage; the middle layer is generally common business components, such as account, tracking, payment, shopping cart, etc.; the top layer is iterative business components with the highest update frequency.
  3. How to collaborate with multiple teams? The division of labor within the team should be flexible, and personnel should not be isolated and fixed, which would result in no one using what they have built. Then, it is necessary to refine the functional modules around specific businesses to solve the problem of redundant construction. Based on this, make the refined modules solid and reliable.

The author's ideal architecture:

The coordination of component relationships is not fixed and has no standard. The quality of coordination becomes a basic criterion for evaluating the quality of architecture.

In practice, there are generally two architectural design approaches:

  1. Protocol-oriented: Using the protocol-oriented programming approach, protocols are used to define specifications at the compilation level, and implementations can be in different places, achieving distributed management and maintenance of components. This approach also follows the principle of dependency inversion and is a good practice in object-oriented programming.

  2. Mediator: Using a mediator to manage the relationships between components, controlling the invocation relationships between components throughout the entire lifecycle of the app.

When considering architectural design, it is more important to achieve decoupling at the functional logic and component division levels, with clear dependencies between upper and lower layers. This structure allows upper-layer components to be easily plugged in and lower-layer components to be more stable. The mediator architectural pattern is easier to maintain this structure. The manageability and scalability of the mediator make the overall architecture maintain stability and vitality in the long run. Therefore, the mediator architecture is the author's ideal architecture.

Case Study:

ArchitectureDemo - GitHub. It adds support for middleware, state machines, observers, and the factory pattern on top of the mediator architecture. In addition, it supports chain calling in usage.

References:

iOS Application Architecture - Casatwy

05 | Linker: How Are Symbols Bound to Addresses?#

Learning with Questions: In the projects I have participated in, why do some compile very quickly while others are slow? After compilation, why do some start quickly while others are slow?

This article will talk about the linker, whose main function is to bind symbols to addresses. Let's start with the compiler~

Why Use a Compiler for iOS Development?#

The code written for iOS is compiled into machine code by a compiler and then executed directly on the CPU.

The reason for not using an interpreter to run the code is that Apple wants the program to be more efficient and faster.

On the other hand, an interpreter can execute code at runtime, which has dynamism and can change the program's logic by updating the code at any time.

The compiler currently used by Apple is LLVM, which is three times faster in compilation speed compared to GCC used before Xcode 5. Apple has also played a leading role in the development of LLVM, allowing LLVM to optimize more for Apple's hardware.

LLVM is essentially a collection of compiler toolchain technologies: the compiler compiles each file to generate Mach-O (executable file); the linker (the lld project in LLVM) merges multiple Mach-O files in the project into one.

Compilation Process:

  1. First, after you write the code, LLVM preprocesses your code, such as embedding macros in the corresponding locations.
  2. Then, LLVM performs lexical analysis and syntax analysis on the code, generating an AST (Abstract Syntax Tree), which is structurally more concise than the code and faster to traverse.
  3. Finally, the AST generates IR (Intermediate Representation), which is a language closer to machine code. The difference is that it is platform-independent, and multiple sets of machine code suitable for different platforms can be generated from IR. For the iOS system, the executable file generated from IR is Mach-O.

What Does the Linker Do at Compile Time?#

The main task of the linker is to complete the binding of variables, function names, and their addresses, and the symbols mentioned at the beginning can be understood as variable names and function names.

During the process of organizing the symbol call relationship, the linker will identify which functions are not called and automatically remove them. In this process, the linker starts from the main function as the source and follows each reference, marking them as live. After the follow-up is completed, the functions that are not marked as live are unused functions. Then, the linker can enable the automatic removal of unused code by turning on the Dead code stripping switch (this switch is enabled by default).

Dynamic Library Linking#

Shared libraries for linking can be divided into static libraries and dynamic libraries:

  • Static libraries are linked at compile time and need to be linked into your Mach-O file. If an update is needed, recompilation is required, and they cannot be dynamically loaded and updated.
  • Dynamic libraries are linked at runtime and can be dynamically loaded using dyld.

There are two ways to load dynamic libraries using dyld: 1) Binding at program startup; 2) Binding when the symbol is first used. To reduce startup time, most dynamic libraries use the second method.

Common Tools: nm tool - view symbol table, otool tool - find required libraries for symbols.

I hope that through this series "An Overview of the iOS Knowledge System", you will continue to explore in depth on your own. Don't just scratch the surface~


Poll: What will you mainly do during the Spring Festival holiday?

A. Visit relatives

B. Travel

C. Organize gatherings

D. Watch dramas

E. Read

F. Code

G. Other

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.