WWDC 2016 and 2017 have both mentioned the principle of startup and the idea of performance optimization, which shows how important startup time is for developers and users. This article will talk about how to accurately measure the startup time of App. The startup time consists of the startup time before main and the startup time after main.
This is Apple’s POWERPOINT presentation at WWDC, and it’s a simple summary of what was done before Main launched. What about the startup time after Main? It’s more up to you to define, some people will take the time between main and the end of didFinishLaunching, some people will take the time between main and the end of the first ViewController’s viewDidAppear. Anyway, I think it can reflect the problem to a certain extent.
Xcode measures the pre-main time
Xcode provides a nice way to test startup time. Just set the environment variable DYLD_PRINT_STATISTICS to 1 in the Edit scheme -> Run -> Arguments. You can see the amount of time consumed in the stages before Main.
Total pre-main time: 341.32 milliseconds (100.0%) Dylib loading time: 154.88 milliseconds (45.0%) Rebase /binding time: 37.20 milliseconds (10.8%) ObjC Setup Time: 52.62 milliseconds (15.4%) Initializer time: 96.50 milliseconds (28.2%) vs intializers: 4.07 milliseconds (1.1%) libMainThreadChecker. Dylib: 30.75 milliseconds (9.0%) AFNetworking: 20.08 milliseconds (5.5%) LDXLog: 10.06 milliseconds (2.9%) Bigger: 7.05 milliseconds (2.0%)Copy the code
There is another way to get a more detailed time simply by setting the environment variable DYLD_PRINT_STATISTICS_DETAILS to 1.
Total time: 1.0 seconds (100%) Total images loaded: 243 (0 from dyld shared cache) Total segments mapped: 721, into 93608 pages with 6173 pages pre-meditation total images loading time: 817.51 milliseconds (78.3%) total load timeinObjC: 63.02 milliseconds (6.0%) Total Debugger pause time: Total dTRACE registration time: 0.07 milliseconds (0.0%) Total Rebase fixups: Total binding fixups: 545 milliseconds (3.5%) Total binding fixups: 545 milliseconds 29.60 milliseconds (2.8%) Total weak Binding fixups time: 2.5milliseconds (0.1%) Total redo shared cached Bindings time: 29.32 Milliseconds (2.8%) Total Bindings Lazily Fixed up: 0 of 0 total timeinInitializers and ObjC +load: 93.76 milliseconds (8.9%) 2.58 milliseconds (0.2%) libBacktraceRecording. Dylib: 3.06 milliseconds CoreFoundation (0.2%) : 1.85 milliseconds (0.1%) Foundation: 2.61 milliseconds (0.2%) libMainThreadChecker. Dylib: AFNetworking: 52.5 milliseconds (1.7 milliseconds) 2.45 milliseconds (0.9 milliseconds) libswiftCore.dylib: 1.16 milliseconds (0.1 milliseconds) libswiftCoreimage.dylib: 1.51 milliseconds (0.1%) Bigger: 3.91 milliseconds (0.3%) Reachability: 1.48 milliseconds (0.1%) ReactiveCocoa: SDWebImage: 1.41 milliseconds (0.1%) SVProgressHUD: Dash (0.1%) Total symbol trie searches: 133246 Total symbol table binary searches: 0 total images defining weak symbols: 30 total images using weak symbols: 69Copy the code
How to measure pre-main time online
If we don’t rely on Xcode, we can also take into account the time before main. Of course, this time metric focuses more on the startup phase that the developer can control. The Initializer section shown in the first figure handles the Initializer and ObjC Load methods for C++ static objects.
Measure the ObjC Load method
How do you calculate this period of time? The most easy to think of is the interception, how to intercept become difficult. Here’s a look at the dyLD source code to see what you can find. The entire initialization process starts with the initializeMainExecutable method. Dyld initializes the dynamic library first and then the executable file of the App.
void initializeMainExecutable()
{
// record that we've reached this step gLinkContext.startedInitializingMainExecutable = true; // run initialzers for any inserted dylibs ImageLoader::InitializerTimingList initializerTimes[allImagesCount()]; initializerTimes[0].count = 0; const size_t rootCount = sImageRoots.size(); if ( rootCount > 1 ) { for(size_t i=1; i < rootCount; ++i) { sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]); } } // run initializers for main executable and everything it brings up sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);Copy the code
So it is not difficult to think of, just Hook all the load functions in the dynamic library, and then click ok. However, many project libraries are managed by Cocoapods, and many use use_frameworks, so our App is not a single executable, but a main image file and a number of dynamic libraries. There is no way to count the execution time of the load function in the dynamic library. The next thing to consider is, how do you find the earliest loaded dynamic library? Then Hook it in its load function.
The load order of a dynamic library is related to the load Commands order and dependencies. As shown in the figure:
In the case of the dynamic libraries we introduced, AFNetworking will load first, and the dependent dynamic libraries will load first. The following is the result of my own test. LDXlog is dependent on Bigger, so AFNetworking is the first load, and then LDXlog is loaded according to load Commands in sequence.
2017-09-23 13:45:01.696816+0800 AAALoadHook[27267:1585198 AAALoadHook[27267:1585198] LDXLog 2017-09-23 13:45:01.707312+0800 AAALoadHook[27267:1585198] Bigger 2017-09-23 13:45:01.710732+0800 AAALoadHook[27267:1585198] Reachability 2017-09-23 13:45:01.710732+0800 AAALoadHook REACtive 2017-09-23 13:45:01.712066+0800 AAALoadHook[27267:1585198] SDWE 2017-09-23 13:45:01.713650+0800 AAALoadHook[27267:1585198] SVProgressHUD 2017-09-23 13:45:01.714499+0800 AAALoadHook[27267:1585198Copy the code
The above test gave me the impression that the dynamic library load was alphabetical, but it wasn’t because I was using a POD managed dynamic library and the order was sorted by CocoaPods. Thank you @donggua@monkey for your answer.
- Reference: www.jianshu.com/p/84936d934…
In other words, just name our statistics library with the beginning of A (our libraries are currently managed using POD) and add A dot internally. To summarize the overall idea again:
- Find the original load dynamic library
- Get all the executables in your App in the load function
- Hook the load function corresponding to the executable
- Count the time of each load function and the total time of all load functions
- Report statistical analysis
Because the code is more, the blog is too long to paste, so if you want to know the source, you can click on this link: github.com/joy0304/Joy…
There are also some things to note about the statistics, that is, do not cause performance problems in order to count the performance, get all the classes and Hook load function is relatively time-consuming, poor control will increase the startup time.
Measuring C++ Static Initializers
InitializeMainExecutable is the initialization of the entrance just mentioned, the function will perform ImageLoader: : runInitializers method, and then invokes the ImageLoader: : doInitialization, Finally, the doModInitFunctions method is executed.
void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
if ( fHasInitializers ) {
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = §ionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
const uint8_t type = sect->flags & SECTION_TYPE;
if ( type == S_MOD_INIT_FUNC_POINTERS ) {
Initializer* inits = (Initializer*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uintptr_t);
for (size_t j=0; j < count; ++j) {
Initializer func = inits[j];
// <rdar://problem/8543820&9228031> verify initializers are in image
if(! this->containsAddress((void*)func) ) { dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath()); } func(context.argc, context.argv, context.envp, context.apple, &context.programVars); } } } } cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize); }}}Copy the code
This code is really long, it reads all the function Pointers from the mod_init_func section, and then executes the function call, These Pointers correspond to functions that our C++ Static Initializers and __attribute__((constructor) modify.
Since they are executed after the load function, we can replace the mod_init_func address in the load function with our hook pointer, and then save the original function pointer to a global data. When executing our hook function, Executes by fetching the original function address from the global array. Post the main code here, and see the link for more: github.com/everettjf/Y…
void myInitFunc_Initializer(int argc, const char* argv[], const char* envp[], const char* apple[], const struct MyProgramVars* vars){
++g_cur_index;
OriginalInitializer func = (OriginalInitializer)g_initializer->at(g_cur_index);
CFTimeInterval start = CFAbsoluteTimeGetCurrent();
func(argc,argv,envp,apple,vars);
CFTimeInterval end = CFAbsoluteTimeGetCurrent();
}
static void hookModInitFunc(){
Dl_info info;
dladdr((const void *)hookModInitFunc, &info);
#ifndef __LP64__
const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;
unsigned long size = 0;
MemoryType *memory = (uint32_t*)getsectiondata(mhp, "__DATA"."__mod_init_func", & size);
#else
const struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;
unsigned long size = 0;
MemoryType *memory = (uint64_t*)getsectiondata(mhp, "__DATA"."__mod_init_func", & size);
#endif
for(int idx = 0; idx < size/sizeof(void*); ++idx){ MemoryType original_ptr = memory[idx]; g_initializer->push_back(original_ptr); memory[idx] = (MemoryType)myInitFunc_Initializer; }}Copy the code
Can C++ Static Initializers even exist? Yes, I wanted to count C++ Static Initializers execution times of all executables in my App in a dynamic library, but dyld has this code:
if ( type == S_MOD_INIT_FUNC_POINTERS ) {
Initializer* inits = (Initializer*)(sect->addr + fSlide);
const size_t count = sect->size / sizeof(uintptr_t);
for (size_t j=0; j < count; ++j) {
Initializer func = inits[j];
// <rdar://problem/8543820&9228031> verify initializers are in image
if(! this->containsAddress((void*)func) ) { dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath()); } func(context.argc, context.argv, context.envp, context.apple, &context.programVars); }}Copy the code
if ( ! This ->containsAddress((void*)func)); this->containsAddress((void*)func)); Not in any other image, so when the other image performs this judgment, it throws an exception. There seems to be no solution to this problem, so our C++ Static Initializers time statistics are a bit inadequate.
Xcode For Static Initializers
Apple in developer.apple.com/videos/play… New tools for tracking time consumption in Static Initializers have been announced. Instruments has added a tool called Static Initializers Tracing, You can easily check the time consumption of each Static Initializer. (I haven’t updated the latest version, so I don’t practice it yet)
Time measurement after Main
Either the end of main to the end of didFinishLaunching or the viewDidAppear of the first ViewController is a measure of the startup time after main. This time statistics some calculation can be directly, but when for a long time need to troubleshoot problems, only two points of time are not convenient, now see more useful way is to start task for the standardization, particles, has some statistics for each task, so convenient late positioning and optimization of the problem.
Optimization?
In fact, many companies have blogs about optimization. Since talking about starting monitoring, write a little personal feel more use of the optimization plan.
- At present, many projects use the POD dynamic library of use_frameworks. There are some optimization schemes for the system’s dynamic library, such as shared cache. However, if our dynamic library becomes too many, it will be time-consuming, so merging the dynamic library is an effective and feasible solution
- Start tasks subdivided, do not need to be initialized in time, do not need to be initialized in the main thread, choose asynchronous delay loading
- Monitor load and Static Initializers time consumption — it’s easy to lose hundreds of milliseconds
- There are many other companies to practice the scheme, I have collected down, can refer to: github.com/joy0304/Joy…