Something that happens after ⌘ + R

As a coder, my daily work is either solving bugs or writing bugs. Some things are not necessarily good for writing bugs, but good for solving bugs.

To a project, when you press ⌘ + R to the main interface to be shown, have you ever wondered what is going on in the process? These primitive things do not help us coding directly, so we can do coding whether we know it or not. But a coder’s job is not only coding, but also debugging. Knowing these things will help us sort out some of the problems.

According to stages, this process can be roughly divided into three stages: compilation stage, APP startup stage and layer rendering stage. These three processes are described in detail below.

Compilation phase

Those who have learned the principle of compilation should know that compilation is mainly divided into four processes: preprocessing, compilation, assembly and linking. The following is roughly in accordance with this approach. IOS compilation process, using CLang as the front end, LLVM as the back end to complete. You use Clang to handle the first stages and LLVM to handle the later stages.

1. The preprocessing

Also known as precompilation, it does some text replacement work. Handle instructions beginning with #, such as:

  • Expansion of macro definition (#define)
  • Expand header files (#include, #import)
  • Handling conditional compilation instructions (#if,#else,#endif)

For example, we define the following macro in our code:

#define APP_VERSION "V1.0.0"

int main(int argc, char * argv[]) {
  char *version = APP_VERSION;
  printf("app version is %s",version);
}
Copy the code

The pre-processing results of macro expansion using Clang-e main.m are as follows:

int main(int argc, char * argv[]) {
    char *version = "V1.0.0";
    printf("version is %s",version);
    return 0;
}
Copy the code

Macros have a lot of pitfalls, try to use other ways instead.

2. Lexical analysis

After the pretreatment, the lexical analyzer (also called scanner) will scan the source code in.m from left to right, identify all kinds of words and keywords according to the lexical rules of the language, and generate the corresponding word attribute words. For example, the following code:

#define APP_VERSION "V1.0.0"

int main(int argc, char * argv[]) {
    char *version = APP_VERSION;
    printf("version is %s",version);
    return 0;
}
Copy the code

After pre-processing, clang-xclang-dump-tokens main.m was used for scanning analysis. The results are as follows:

int 'int' [StartOfLine] Loc=<main.m:14:1> identifier 'main' [LeadingSpace] Loc=<main.m:14:5> l_paren '(' Loc=<main.m:14:9> int 'int' Loc=<main.m:14:10> identifier 'argc' [LeadingSpace] Loc=<main.m:14:14> comma ',' Loc=<main.m:14:18> char 'char' [LeadingSpace] Loc=<main.m:14:20> star '*' [LeadingSpace] Loc=<main.m:14:25> identifier 'argv' [LeadingSpace] Loc=<main.m:14:27> l_square '[' Loc=<main.m:14:31> r_square ']' Loc=<main.m:14:32> r_paren ')' Loc=<main.m:14:33> l_brace '{' [LeadingSpace] Loc=<main.m:14:35> char 'char' [StartOfLine] [LeadingSpace] Loc=<main.m:18:5> star '*' [LeadingSpace] Loc=<main.m:18:10> identifier 'version' Loc=<main.m:18:11> equal '=' [LeadingSpace] Loc=<main.m:18:19> string_literal '"V1.0.0"' [LeadingSpace] Loc=<main.m:18:21 <Spelling=main.m:12:21>> semi '; ' Loc=<main.m:18:32> identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:19:5> l_paren '(' Loc=<main.m:19:11> string_literal '"version is %s"' Loc=<main.m:19:12> comma ',' Loc=<main.m:19:27> identifier 'version' Loc=<main.m:19:28>  r_paren ')' Loc=<main.m:19:35> semi '; ' Loc=<main.m:19:36> return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:20:5> numeric_constant '0' [LeadingSpace] Loc=<main.m:20:12> semi '; ' Loc=<main.m:20:13> r_brace '}' [StartOfLine] Loc=<main.m:21:1> eof '' Loc=<main.m:21:2>Copy the code

As you can see from the above, each word or character is marked with the number of columns and lines so that clang can quickly locate errors in the code if it encounters problems during compilation.

3. Grammatical analysis

Next comes the parsing. Through this stage, the results derived from the previous stage are parsed into an abstract syntax tree — AST. Suppose our source code looks like this and has been preprocessed:

#define APP_VERSION "V1.0.0"

int main(int argc, char * argv[]) {
    char *version = APP_VERSION;
    printf("version is %s",version);
    return 0;
}
Copy the code

After running the clang command clang-xclang-ast – dump-fsyntax-only mian. M, the syntax tree is as follows:

. FunctionDecl 0x7ffe55884228 <main.m:14:1, line:21:1> line:14:5 main 'int (int, char **)' |-ParmVarDecl 0x7ffe55884028 <col:10, col:14> col:14 argc 'int' |-ParmVarDecl 0x7ffe55884110 <col:20, col:32> col:27 argv 'char **':'char **' `-CompoundStmt 0x7ffe55884568 <col:35, line:21:1> |-DeclStmt 0x7ffe55884390 <line:18:5, col:32> | `-VarDecl 0x7ffe558842e8 <col:5, line:12:21> line:18:11 used version 'char *' cinit | `-ImplicitCastExpr 0x7ffe55884378 <line:12:21> 'char *' < ArrayToPointerDecay > | ` - StringLiteral zero x7ffe55884348 < col: 21 > 'char [7]' lvalue "V1.0.0 | - x7ffe558844b0 CallExpr 0" <line:19:5, col:35> 'int' | |-ImplicitCastExpr 0x7ffe55884498 <col:5> 'int (*)(const char *, ...) ' <FunctionToPointerDecay> | | `-DeclRefExpr 0x7ffe558843a8 <col:5> 'int (const char *, ...) ' Function 0x7ffe55088570 'printf' 'int (const char *, ...) ' | |-ImplicitCastExpr 0x7ffe55884500 <col:12> 'const char *' <BitCast> | | `-ImplicitCastExpr 0x7ffe558844e8 <col:12> 'char *' <ArrayToPointerDecay> | | `-StringLiteral 0x7ffe55884408 <col:12> 'char [14]' lvalue "version is %s" | `-ImplicitCastExpr 0x7ffe55884518 <col:28> 'char *' <LValueToRValue> | `-DeclRefExpr 0x7ffe55884440 <col:28> 'char *' lvalue Var 0x7ffe558842e8 'version' 'char *' `-ReturnStmt 0x7ffe55884550 <line:20:5, col:12> `-IntegerLiteral 0x7ffe55884530 <col:12> 'int' 0Copy the code

Each node in the abstract syntax tree is also marked with a specific location in the source code for easy problem location. There is a lot of knowledge about abstract syntax trees that I won’t explain in detail here.

Static analysis

After converting the source code into an abstract syntax tree, the compiler can parse the tree. Static analysis improves code quality by checking for errors such as methods that are called but not defined, variables that are defined but not used, etc. You can also use Xcode’s own static analysis tool (Product -> Analyze) or some third-party static analysis tools (such as Facebook’s Infer) to perform in-depth analysis.

Sometimes the compiler’s built-in static analysis does not meet our daily development needs. So we can use scripts to customize a set of analysis solutions and put them into an integrated environment. Each time the code is committed, the script is triggered for static analysis, warning if an error occurs, and the code fails to be committed. Too high development quality in turn.

If you are interested, take a look at the Clang static analysis source code to see which syntax is statically analyzed.

5. Generate code and optimize

After preprocessing and analysis using Clang, LLVM code is generated. Same code as before:

#define APP_VERSION "V1.0.0"

int main(int argc, char * argv[]) {
    char *version = APP_VERSION;
    printf("version is %s",version);
    return 0;
}
Copy the code

Clang -o3 -s -emit-llvm main.m -o main.ll: clang -o3 -s -emit-llvm main.m -o main.ll

; ModuleID = 'main.m' source_filename = "main.m" target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-apple-macosx10.13.0" @. STR = private unnamed_addr constant [7 x i8] c"V1.0.0\00", align 1 @.str.1 = private unnamed_addr constant [14 x i8] c"version is %s\00", align 1 ; Function Attrs: Nounwind SSP uwtable define i32@main (i32, i8** nocapture readnone) local_unnamed_addr #0 { %3 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str.1, i64 0, i64 0), i8* getelementptr inbounds ([7 x i8], [7 x i8]* @.str, i64 0, i64 0)) ret i32 0 } ; Function Attrs: nounwind declare i32 @printf(i8* nocapture readonly, ...) local_unnamed_addr #1 attributes #0 = { nounwind ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "Target - the features" = "+ cx16, + FXSR, + MMX and sse +, + sse2, + sse3, + sse4.1, + ssse3, + x87" "unsafe - the fp - math" = "false" "use-soft-float"="false" } attributes #1 = { nounwind "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "Target - the features" = "+ cx16, + FXSR, + MMX and sse +, + sse2, + sse3, + sse4.1, + ssse3, + x87" "unsafe - the fp - math" = "false" "use-soft-float"="false" } ! llvm.module.flags = ! {! 0,! 1,! 2,! 3,! 4,! 5}! llvm.ident = ! {! 6}! 0 =! {i32 1, !" Objective-C Version", i32 2} ! 1 =! {i32 1, !" Objective-C Image Info Version", i32 0} ! 2 =! {i32 1, !" Objective-C Image Info Section", !" __DATA, __objc_imageinfo, regular, no_dead_strip"} ! 3 =! {i32 4, !" Objective-C Garbage Collection", i32 0} ! 4 =! {i32 1, !" Objective-C Class Properties", i32 64} ! 5 =! {i32 1, !" PIC Level", i32 2} ! 6 =! {!" Apple LLVM Version 9.0.0 (clang-900.0.39.2)"}Copy the code

If you look at the main method, it doesn’t matter if you don’t understand it, I don’t understand it either. Just understand the process.

LLVM will then compile and optimize the code, such as optimization for global variables, loop optimization, tail recursion optimization, etc., which I don’t know too much about, so I can’t talk too much about. If you want to know more about it, you can take a look at this article: LLVM full time Optimization.

Finally, output assembly code.

6. Assembly

At this stage, the assembler converts readable assembly code into machine code. The end product is the object file ending in.o.

For the following part of the code:

#define APP_VERSION "V1.0.0"

int main(int argc, char * argv[]) {
    char *version = APP_VERSION;
    printf("version is %s",version);
    return 0;
}
Copy the code

We can use the clang command clang -c main.m to generate the target file mian. I’m not going to write what I opened, because it’s binary and I can’t read it.

Links to 7.

This phase links the object file generated in the previous phase with the referenced static library to generate the executable file.

We can use the clang command clang main.m to generate the executable a.out (a.out by default). Then use the file a.out command to check its type:

a.out: Mach-O 64-bit executable x86_64
Copy the code

You can see that executables are of the Mach-O type, and executables on both MAC OS and iOS platforms are of this type. Because I’m using an emulator, the processor instruction set is X86_64.

The compilation phase is now complete.

8. A complete build in Xcode

Finally, let’s take a look at the Build log in Xcode to walk through the process. Open Xcode’s Log Navigator and select Build to see the Log for this Build:

The log is segmented by target. In the current project, three libraries YYCache, YYImage and AFNetworking are introduced through Pod. Besides, there is a PODs-test and target of the project itself. The log format is the same between each target, so we only analyze one target. This analysis is only for the project itself target, also known as Test. It looks like this:

It looks very messy. After finishing it, I will repeat the following process:

  1. Compile information is written to secondary files to create the compiled file schema (test.app).
  2. Process packaging information.
  3. Execute the CocoaPods pre-compile script. For example, hereCheck Pods Manifest.lock.
  4. Compile various.m files (.h files are not involved).
  5. Link the required framework.
  6. Compile ImageAssets.
  7. Compile the Storyboard and related files.
  8. Process the info.plist file.
  9. Link Storyboards.
  10. Execute CocoaPods related scripts, which you can view in Build Phases.
  11. Create the.app file.
  12. Sign the.app file

Here we’ll elaborate on step 4. We select the log of one of the files viewController.m for analysis:

To sort out the log information:

1. CompileC /... /Test.build/Objects-normal/x86_64/ViewController.o Test/ViewController.m normal x86_64 objective-c com.apple.compilers.llvm.clang.1_0.compiler 2.cd /Users/zhoubo/Test
3. export LANG=en_US.US-ASCII
   export PATH="/Applications/Xcode.app/Contents/Developer/.. /sbin"4. clang -x objective-c -arch x86_64 -fmessage-length=0... -fobjc-arc... -Wno-missing-field-initializers... -DDEBUG=1... -isysroot ... / iphonesimulator11.2. SDK -I ONE PATH -f ONE PATH -c /.. /ViewController.m -o /.. /ViewController.oCopy the code

The corresponding explanations are as follows:

  1. The starting point of a task is expressed in log.
  2. The corresponding working directory is displayed.
  3. Perform Settings on the LANG and PATH environment variables.
  4. Clang command start:

    -x: objective-c-arch x86_64: the processor instruction set is x86_64-fobjC-arc: a series of instructions-fAt the beginning, specify that this file uses the ARC environment. You can set ARC support for each file via Build Phases. -wno-mission-field-initializers: a series of directives starting with -w, warning options for compilation, which can be customised with -ddebug =1: some that start with -d, meaning pre-compiled macros. -isysroot ... / iphonesimulator11.2. SDK: The iOS SDK version used during compilation. -i: writes the compilation information to a file. -f: the framework required during the linking process. -C: compiles the fileCopy the code

9. About dSYM files

Every time we compile, we generate a dSYM file. In this file, the hexadecimal function address mapping table is stored. In the binary that the APP executes, methods are called by address. When crash occurs, address mapping can be performed through dSYM files to find the specific function call stack.

App Startup Phase

In the previous phase, the final product was an executable file in the format of Mach-O. For this stage, I’ll start with this file and describe the APP startup process in detail.

1. Process overview

This process is divided into several stages, a simple comb, can make the brain has a clear brain circuit, not more and more confused.

  • System preparation.
  • Load dyLD into App process (DYLD).
  • Load Dylibs required by the App.
  • Rebase & Bind.
  • Objc setup.
  • Initializers.
  • Mian ().

An official flow chart:

2. Concept explanation

Before going through the whole process, let’s explain two concepts: Mach-O files and dyLD.

.Mach-O

Mach-o is a file format used in iOS, MacOS, WatchOS and other Apple operating systems. This file format can be used for the following files:

  • Executable files
  • Dylib dynamic libraries
  • Bundles cannot be connected to dynamic libraries and can only be loaded via dlopen()
  • Image here means one of various Executable, Dylib, or Bundle, as described below.
  • A collection of Framework dynamic libraries and corresponding header and resource files.

The mach-o file format is as follows:

  • Header, which contains the CPU architecture of the file, such as x86, ARM7, ARM64, etc.
  • Load Commands, which contains the organization structure of files and how they are laid out in virtual memory.
  • Data, which contains each segment required for Load Commands, and each segment contains multiple sections. When running an executable, the virtual memory system maps segments to the process’s address space.

Create executable file (a.out); use size to view the segment content of this executable.

xcrun size -x -l -m a.out
Copy the code

The following results can be obtained:

Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
  Section __text: 0x43 (addr 0x100000f30 offset 3888)
  Section __stubs: 0x6 (addr 0x100000f74 offset 3956)
  Section __stub_helper: 0x1a (addr 0x100000f7c offset 3964)
  Section __cstring: 0x15 (addr 0x100000f96 offset 3990)
  Section __unwind_info: 0x48 (addr 0x100000fac offset 4012)
  total 0xc0
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
  Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
  Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
  Section __objc_imageinfo: 0x8 (addr 0x100001018 offset 4120)
  total 0x20
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000
Copy the code

To make a long story short:

  • Segment __PAGEZERO. The size is 4GB. The first 4GB of the process address space is mapped as unreadable, unwritable, and unexecutable.
  • Segment __TEXT. Contains executable code, mapped read-only and executable.
  • Segment __DATA. Contains data that will be changed, mapped in both read-write and non-executable ways.
  • Segment __LINKEDIT. Contains metadata for methods and variables, code signatures, and more.
dyld

Dynamic loader. It is open source and you can read the source code if you are interested. Dyld1 is outdated. Don’t try to understand it. Dyld2 is most commonly used at present. WWDC2017 Apple launched dyLD3, currently only available on iOS App, should be popular later. At the end of this phase, dyLD3 will be described in more detail than is described here.

Let’s start with a formal introduction to the startup process.

3. System preparation

After clicking APP, the system did a lot of things in the process of loading dyLD dynamic loader, which can be roughly divided into the following stages:

Most of you haven’t studied this in depth, and NEITHER have I. So I try to make things as simple as possible and try to explain these processes in the simplest way possible.

  • After clicking APP, the system will create a process. Then use theload_init_programFunction loads the system initialization process. And then call it inside the methodload_init_program_at_path. throughload_init_program_at_pathThe method call__mac_execve.
  • __mac_execveThe function starts a new process and task, calledexec_activate_image.
  • exec_activate_imageFunction distributes memory mapping functions in binary format.Mach-OThe file will beexec_mach_imgactTo deal with.
  • inexec_mach_imgactFunction, will detectMach-OHeader, parse its architecture and other information, whether the file is legal, etc.; First copyMach-OFile into memory; Then copyMach-OFile into memory; Then dyLD related treatment; Finally, release resources.
  • load_machfileFunction is responsible forMach-OFile loading related work. Allocate executable memory for the current task. loadingMach-OThe load Command section of the Base data section execution, prevent overflow vulnerability attacks, set ASLR, etc.; The last toexec_mach_imgactReturn the result.
  • parse_machfileAccording to theload_commandInformation to select different functions to load data. The one that’s used isswitch-caseStatement, and the types handled areLC_LOAD_DYLINKER,LC_ENCRYPTION_INFO_64And so on.
  • In the previous step, there was a case ofLC_LOAD_DYLINKER. Enter this case three times and existdylinker_commandOrder, will be laterperformload_dylinker()Loading dyld.

4. Load dyLD into the App process

In the source of dyld, there is a dyLDstartup. s file. This file defines different boot methods for different CPU architectures. The __dyLD_START method is executed, then the dyLDbootstrap ::start() method is called, and finally the dyld::_main() method in dyld.cppp is called. Part of the code is as follows:

__dyld_start:
  pushq $0    # push a zero for debugger end of frames marker
  movq  %rsp,%rbp # pointer to base of kernel frame
  andq    $- 16,%rsp       # force SSE alignment
  
  # call dyldbootstrap::start(app_mh, argc, argv, slide)
  movq  8(%rbp),%rdi  # param1 = mh into %rdi
  movl  16(%rbp),%esi # param2 = argc into %esi
  leaq  24(%rbp),%rdx # param3 = &argv[0] into %rdx
  movq  __dyld_start_static(%rip), %r8
  leaq  __dyld_start(%rip), %rcx
  subq   %r8, %rcx  # param4 = slide into %rcx
  call  __ZN13dyldbootstrap5startEPK12macho_headeriPPKcl  

      # clean up stack and jump to result
  movq  %rbp,%rsp # restore the unaligned stack pointer
  addq  $16,%rsp  # remove the mh argument, and debugger end frame marker
  movq  $0,%rbp   # restore ebp back to zero
  jmp *%rax   # jump to the entry point
Copy the code

The _main() method contains the startup process of the App, and finally returns the address of the main method of the App. The code is omitted here, and only the process is marked:

uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[])
{ 
    // Setting up context, initializing necessary parameters, parsing environment variables, etc.try {
    // instantiate ImageLoader for main executable
    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
    sMainExecutable->setNeverUnload();
    gLinkContext.mainExecutable = sMainExecutable;
    gLinkContext.processIsRestricted = sProcessIsRestricted;
    
    // load shared cache
    checkSharedRegionDisable();
  #if DYLD_SHARED_CACHE_SUPPORT
    if( gLinkContext.sharedRegionMode ! = ImageLoader::kDontUseSharedRegion ) mapSharedCache();#endif
  
    // load any inserted libraries
    if( sEnv.DYLD_INSERT_LIBRARIES ! =NULL ) {
      for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib ! =NULL; ++lib) loadInsertedDylib(*lib); }...// link main executable
    gLinkContext.linkingMainExecutable = true;
    link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL.NULL));
    gLinkContext.linkingMainExecutable = false;
    if ( sMainExecutable->forceFlat() ) {
      gLinkContext.bindFlat = true;
      gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
    }
    
    // get main address
    result = (uintptr_t)sMainExecutable->getMain(); .return result;
}
Copy the code

5. Load the dynamic library required by the App

As mentioned above, an image is actually a type of Mach-O file that includes Executable, Dylib, or Bundle. In section dyld: : _main () function can be seen, dyld will by calling instantiateFromLoadedImage choose imageLoader load corresponding to the executable file.

Then through mapSharedCache () function/System/Library/Caches/com. Apple. Dyld/dyld_shared_cache_arm64 Shared dynamic Library loaded into memory, This is also the dynamic library sharing mechanism implemented by different apps. The shared dynamic library in the virtual memory of different apps will map the same physical memory through the vm_map of the system, so as to realize the shared dynamic library.

The loadInsertedDylib() function is then called to load the dynamic libraries in the environment variable DYLD_INSERT_LIBRARIES. The loadInsertedDylib dynamic library doesn’t do much work. The main work is to call the load function. Dlopen also calls the load function to load the dynamic library.

The link() function is then called to recursively link to the library that the program depends on. Generally, an App relies on about 100-400 dynamic libraries. You can run the otool -l Test command to view the dynamic library required by the Test project as follows:

The/usr/lib/libsqlite3. Dylib (compatibility version 9.0.0, Current version 274.6.0) /usr/lib/libz.1.dylib (compatibility version 1.0.0, The current version 1.2.11)/System/Library/Frameworks/Accelerate framework/Accelerate (compatibility version 1.0.0, The current version 4.0.0)/System/Library/Frameworks/AssetsLibrary framework/AssetsLibrary (compatibility version 1.0.0, The current version 1.0.0)/System/Library/Frameworks/CoreFoundation framework/CoreFoundation (compatibility version 150.0.0, The current version 1450.14.0)/System/Library/Frameworks/CoreGraphics framework/CoreGraphics (compatibility version 64.0.0, The current version 1129.2.1)/System/Library/Frameworks/ImageIO framework/ImageIO (compatibility version 1.0.0, The current version 0.0.0)/System/Library/Frameworks/MobileCoreServices framework/MobileCoreServices (compatibility version 1.0.0, The current version 822.19.0)/System/Library/Frameworks/QuartzCore framework/QuartzCore (compatibility version 1.2.0, The current version 1.11.0)/System/Library/Frameworks/Security. The framework/Security (compatibility version 1.0.0, The current version 58286.32.2)/System/Library/Frameworks/SystemConfiguration framework/SystemConfiguration (compatibility Version 1.0.0, current version 963.30.1)/System/Library/Frameworks/UIKit framework/UIKit (compatibility version 1.0.0, The current version 3698.33.6)/System/Library/Frameworks/Foundation. The framework/Foundation (compatibility version 300.0.0, /usr/lib/libobjc.a. dylib (Compatibility version 1.0.0, /usr/lib/libsystem. dylib (Compatibility version 1.0.0, current version 1252.0.0)Copy the code

Third-party libraries in CocoaPods are usually loaded as static libraries, so using otool -l [filename] you won’t see the libraries in Pod. But if you add use_frameworks to your Podfile! , that is, when loaded in dynamic library mode, you will see, as shown above.

Finally, get the address of the application’s main function and return.

6.Rebase & Bind

These two processes occur not after the _main() method returns above, but rather in the “Link main executable” step of the previous section.

In order to ensure the application security, Apple uses two technologies: ASLR (Address Space Layout Randomization) and Code Sign.

ASLR stands for “address space layout randomization”. When the App starts, the application is mapped to a logical address space. If the address is fixed, it is easy to calculate the function address based on the address + offset, which is attacked. ASLR makes this address random, preventing an attacker from directly locating the attack code.

A Code sign is a Code signature. Apple uses two layers of asymmetric encryption to ensure secure App installation. When Code Sign is done, it is encrypted for each page so that when dyLD is loaded, it can be verified independently for each page.

Because the address is random using ASLR, you need to add an offset to the true method address. Call a method whose address may belong to a Mach-O file or to another Mach-O file.

Rebase fixes the internal symbolic address, that is, the pointer to the resource inside the current Mach-O file, by adding an offset.

Bind fixes the external symbolic address, that is, the pointer to the external Mach-O file. This process requires querying the symbol table to point to other Mach-O files, which can be time-consuming.

Here’s one of the official images:

In short, the address is wrong when loading the dynamic library in the previous step.

At this point, mach-O loading is done, and it’s time for iOS.

7.Objc Setup

Objc is a dynamic language, and this step mainly loads Runtime stuff. I will mainly do the following things:

  • Register the related classes in the global table.
  • Register the methods in Category and Protocol with the corresponding classes.
  • Make sure that the Selector is unique.

This step deals with custom classes and methods. The Runtime initialization of most system classes is already done in Rebase and Bind.

8.Initializers

This step does some class initialization. This is a recursive process that initializes the dependent dynamic libraries and then initializes your own custom classes. The main things to do are:

  • Call the Objc class+[load]Methods.
  • Call C/C++ marked as__attribute__(constructor)Methods.
  • C++ static global ratio variable creation of non-primitive types.

Swift has eliminated the +load method and officially recommends using the Initialize method to reduce App startup time.

9.Main

Finally, we arrive at the main() method.

C-based programs typically start with the main() method, which iOS automatically creates for you. The code is simple:

int main(int argc, char * argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil.NSStringFromClass([AppDelegate class])); }}Copy the code

The UIApplicationMain method used here is declared as follows:

UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nonnull * _Null_unspecified argv, NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);
Copy the code

  • Argc and argv are passed directly to UIApplicationMain for processing.
  • PrincipalClassName Specifies the class name of the application. This class must beUIApplicationType or a subclass of it. If nil, it is usedUIApplicationClass.
  • DelegateClassName, specifies the application proxy class. This class must be followedUIApplicationDelegateThe agreement.
  • UIApplicationMain is created based on principalClassNameUIApplicationObject, and creates a Delegate object based on delegateClassName, which is assigned toUIApplicationObject’s delegate property.
  • The App is then placed in the Main Run Loop environment to respond to and process user interaction events.

About some methods in the AppDelegate:

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // The notification process has started, but has not finished displaying.
    return YES;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // The program is ready to run. Last chance to operate before the page is displayed.
    return YES;
}


- (void)applicationWillResignActive:(UIApplication *)application {
    // App loses focus and enters inactive state. The main examples are: call, some system pop-ups, double click the home button, pull down to show the system notification bar, pull up to show the system control center, etc.
}


- (void)applicationDidEnterBackground:(UIApplication *)application {
    // App goes to the background.
}


- (void)applicationWillEnterForeground:(UIApplication *)application {
    // The App enters the foreground. Cold starts do not receive this notification.
}


- (void)applicationDidBecomeActive:(UIApplication *)application {
    // App gets focus and is active. Both cold and hot starts receive this notification.
}


- (void)applicationWillTerminate:(UIApplication *)application {
    // When an application is about to exit, you can save data and some cleanup before exit in this method.
}

- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
    // Received a memory warning, free some memory.
}
@end
Copy the code

10.One more thing

So dyLD3, which I talked about in more detail, is here. Dyld3 is a new dynamic loader introduced by WWDC 2017. Pair with DYLD2 as shown below:

The difference between the two, popular point is: dyLD2 all the process is carried out at the start, each start will talk about all the process again; Dyld3 is divided into two parts, the upper part of the dashed line is executed when the App is downloaded, installed and updated and the result is written to the cache, and the lower part of the dashed line is executed every time the App is started.

This reduces dyLD loading steps and speeds up APP startup time. Dyld3 is currently only available in Apple apps, not developers. It should catch on later.

According to the above analysis process, we can generally summarize the following aspects if we want to optimize the startup of App:

  • Reduce the introduction of dynamic libraries. If it is an internal custom component, you can combine some similar components into one.
  • To reduce Rebase & Bind time, reduce__DATAThe number of Pointers in.
  • To reduce Runtime registration time, reduce categories, and reduce useless classes and selectors.
  • Try not to+[load]Write something in the method, subtract__atribute__((constructor))To reduce non-basic C++ static constant creation.
  • Initialize some third-party libraries as they are used, lazy load, don’t put them all in an AppDelegate.
  • Use the Swift.

Layer Render phase

I’ve done a lot of prep work, and I’ve reached the rendering display.

The layout process of layers (in this case, automatic layout) is mainly divided into three steps: setting constraints, updating layout, and rendering view. I’m going to talk about it in terms of the view Controller lifecycle.

1. View layout process

Update Cycle

When the program starts, the App will be placed in the Main Run Loop to respond to and process user interaction events. RunLoop is simply a loop that lasts as long as the App is not killed. Each cycle can be thought of as an iterative cycle in which user interaction events are processed and handled accordingly. After the various events have been processed, the control flow returns to the Main Run Loop and updates the view, which then moves to the next Loop. The whole process is shown below:

During the Update Cycle phase, the view is redrawn based on the calculated new frame. This process is fast, so the user doesn’t feel the lag. Because views are updated periodically, sometimes changing constraints, adding views, or modifying frames does not immediately redraw the view. This process will be described in more detail.

The constraint

The frame of a view contains the position and size of the view. The frame (and the current coordinate system) can be used to determine the position of the view. The essence of constraints is to set up a series of relations, which will be converted into a series of linear equations when calculating the layout. X, Y,width and height can be obtained by solving the linear equations, so as to determine the view position. This stage, from subview to Super View, prepares messages for the next stage of the layout.

updateConstraints()

This method is used to dynamically change view constraints in automatic layout. In general, this method should only be overridden and should not be called manually. During development, some static constraints can be set in the view initialization method or in the viewDidLoad() method; For some dynamic constraints, for example, UILabel sometimes needs to change the size with the word count of the text, and dynamic modification constraints need to be modified. In this case, you can override this method and write the code of dynamic modification constraints in the secondary method.

There are also actions that mark the view and trigger this method automatically in the next Update cycle:

  • Enable/disable constraints.
  • Change the size or priority of the constraint.
  • Change the view hierarchy.

setNeedsUpdateConstraints()

If you want your view to be bound to call the updateConstraints() method in the next Update Cycle, you can call this method to mark your view, The updateConstraints() method is called if necessary in the next Update Cycle.

I say “if necessary” because if the system detects no change in the view, this method will not be called, even if it is flagged, to avoid performance cost. So it’s flagged, just telling the system to check if it wants to update the constraint. Here are some ways to do the same.

updateConstraintsIfNeeded()

If you don’t want to wait until the end of the Run loop, when you enter the Update cycle, to check the tag and update the constraint. You can call this method if you want to check the marked view immediately and update the constraints. Again, calling this method checks only those views that are tagged and, if necessary, calls the updateConstraints() method.

invalidateIntrinsicContentSize()

Some views (such as UILabel) have the intrinsicContentSize property, which is the inherent size based on the view content. You can also by overloading to customize the size, overloading, you need to call invalidateIntrinsicContentSize () method to mark intrinsicContentSize has expired, need to be the next update to recalculate the cycle.

layout

The next step is the layout after calculating the size and location of the view based on the constraints. This section is from the super view to the subview, setting the view’s center and bounds using the size and position calculated in the previous step.

layoutSubviews()

This method repositions and resizes the view and its subviews. This method is expensive because it handles the layout of the current view and its view, and calls the view’s layoutSubviews(), layer by layer. Again, this method should only be overridden and should not be called manually. Override this method when you need to update the frame view.

Some operations may trigger this method, and indirect triggering is much less costly than a manual call. This method can be triggered in the following situations:

  • Modify the view size.
  • Add A View.
  • UIScrollView rolling.
  • Device rotation.
  • Update view constraints

These situations can either tell the system view that the frame needs to be recalculated and call layoutSubviews(), or trigger the layoutSubviews() method directly.

setNeedsLayout()

This method flags the view and tells the system that the view layout needs to be recalculated. In the next Update cycle, the layoutSubviews() method of the view is called. Again, the system will only call it if necessary.

layoutIfNeeded()

SetNeedsLayout is a tagged view, and the layoutSubviews() method may be called in the next Update cycle. LayoutIfNeeded () tells the system to call layoutSubviews() immediately. Of course, calling layoutIfNeeded() only checks to see if the view needs to be refreshed, and calls layoutSubviews() if necessary. If you call layoutIfNeeded() twice in the same Run loop and there is no view update between the two calls, layoutSubviews() will not fire the second time.

This method is useful when doing constraint animations. Call this method to ensure that the other views have been updated before animation. This method is then called to animate to the new state after setting the new constraint in the animation block. Such as:

[self.view layoutIfNeeded];
  [UIView animateWithDuration:1.0 animations:^{
    [self changeConstraints];
    [self.view layoutIfNeeded];
  }];
Copy the code

Apply colours to a drawing

The display of a view includes colors, text, images, and Core Graphics drawings. Similar to the constraint and layout steps, there are methods to refresh the render. This process is from the super view to the subview.

draw(_:)

UIView’s draw method (drawRect in OC) is used to draw the contents of the view. It only works on the current view, not its subviews. Again, this method should be triggered by another method and not called manually.

setNeedsDisplay()

This method is similar to setNeedsLayout() in the layout. Calling this method marks the view, and then the next Update Cycle system iterates through the marked view, calling its draw() method to redraw it. Most UI components, if updated, are marked and redrawn in the next Update cycle. You generally do not need to call this method explicitly.

This step has no method like layoutIfNeeded() to refresh immediately. It usually doesn’t matter if you wait until the next Update cycle.

The three links

The layout process is not one-way, but rather an iterative process of constraint-layout. It is possible for the layout process to affect constraints that trigger updateConstraints(). Just determine the layout, decide if you need to redraw it, and show it off. After this round is complete, the next runloop is entered. Their general process is as follows:

The methods of the three processes mentioned above are similar and confusing to remember. You can compare your memory by the following table:

Methods effect The constraint layout Apply colours to a drawing
Refresh method, can be overloaded, not directly called updateConstraints layoutSubviews draw
Mark the refresh method so that the view invokes the refresh method in the next Update cycle setNeedsUpdateConstraints

invalidateIntrinsicContentSize
setNeedsLayout setNeedsDisplay
updateConstraintsIfNeeded layoutIfNeeded
Trigger the refresh method operation Enable/disable constraints

Change the size or priority of the constraint

Changing the view hierarchy
Modifying view size

Add A View

UIScrollView rolling

Rotating equipment

Update view constraints
Modifying view bounds

2.View Controller lifecycle

When looking for a job, schools are often asked about the VC life cycle. I’ve been asking this question a lot lately when INTERVIEWING other people. Neither I, when I was recruited, nor anyone else I interviewed, even if they had worked for three or five years, could answer this question well.

This is a basic problem, not too technically difficult, and should be mastered.

Single View Controller lifecycle

Describe the individual View Controller lifecycle in method call order as follows:

  • Called when the load class is loaded, before the main function.

  • The Initialize class is called the first time it is initialized, after the main function.

  • Class initializes the associated method [initWithCoder:] when called using storeBoard. [initWithNibName: bundle:] when using custom NIB files. There are other init methods that are called when you normally initialize a class.

  • LoadView starts loading the view, there is no view until then. Unless manually called, it will only be called once during the View Controller lifecycle. in

  • ViewDidLoad View Controller is only called once in its lifetime. Class member variables, subviews, and other data are initialized in this method.

  • The viewWillAppear view is called just before it is displayed.

  • ViewWillLayoutSubviews are going to lay out the subviews.

  • ViewDidLayoutSubviews has completed the subview layout, and the first time I get the frame of the view. Any code that depends on layout or size should be placed in this method. In the previous method, the view didn’t have a layout, frame was all 0; In later methods, the layout or position variables may change due to some modification.

  • The viewDidAppear view shows the completion of the call.

  • Called when the viewWillDisappear view is about to disappear.

  • ViewDidDisappear is called when the view has disappeared.

  • Called when the Dealloc View Controller is released.

Each View Controller has its own method call timing for transitions

Different transition mode, between the two VC method call order is different. The common methods are as follows:

  • Navigation

    Push the operation

    • New viewDidLoad
    • Current viewWillDisappear
    • New viewWillAppear
    • New viewWillLayoutSubviews
    • New viewDidLayoutSubviews
    • Current viewDidDisappear
    • New viewDidAppear

      Pop operation (New of previous step becomes Current here, same below)

    • Current viewWillDisappear

    • New viewWillAppear
    • Current viewDidDisappear
    • New viewDidappear
  • Page Curling (UIPageViewControllerTransitionStylePageCurl)

    Normal Normal page turning operations

    • New viewDidLoad
    • Current viewWillDisappear
    • New viewWillAppear
    • New viewWillLayoutSubviews
    • New viewDidLayoutSubviews
    • Current viewDidDisappear
    • New viewDidAppear

      Canceled in the middle of saying Canceled

    • New viewWillAppear

    • New viewWillAppear
    • Current viewWillDisappear
    • New viewWillLayoutSubviews
    • New viewDidLayoutSubviews
    • New viewWillDisappear
    • Current viewWillAppear
    • New viewDidDisappear
    • Current viewDidAppear
  • Page Scrolling (UIPageViewControllerTransitionStyleScroll)

    Normal Slide to turn pages

    • New viewDidLoad
    • New viewWillAppear
    • Current viewWillDisappear
    • New viewWillLayoutSubviews
    • New viewDidLayoutSubviews
    • New viewDidAppear
    • Current viewDidDisappear

      Canceled in the middle of a Canceled flight

    • New viewWillAppear

    • Current viewWillDisappear
    • Current viewWillAppear
    • Current viewDidAppear
    • New viewWillDisappear
    • New viewDidDisappear

As you can see, the life cycle method call sequence is different between the two View cotrollers for different special methods. Is it very confusing, do not memorize, just need to know this case, in the development is to pay attention to it.

conclusion

The above is basically all the process of a project from compilation to startup. Understanding this process can help us develop better. Because the article is quite long, there are inevitably some mistakes. Please point out if you find it and I will revise it as soon as possible.

reference

  1. objc-Issues-Build-Tools
  2. In-depth understanding of the iOS App startup process
  3. XNU, DYLD Source Analysis of Mach-O and Dynamic Library loading Process (PART 1)
  4. XNU, DYLD source analysis, Mach-O and dynamic library loading process (part 2)
  5. Demystifying iOS Layout
  6. The Inconsistent Order of View Transition Events