How to optimize iOS projects?

How to optimize iOS projects?

1. Structure and Architecture

1.1 Structure

There are two main points in the structure mentioned here: 1. File directory classification 2. Third-party library management

1.1.1 File Directory Classification

For ease of management, *** keep the project display directory in Xcode consistent with the actual storage directory

In addition, generally classified by business module, the first-level directory can be divided according to MVC format or according to business module

Take the most common Model View Controller architecture as an example

  • Take a basic e-commerce project as an example. The four tabbarItems correspond to four modules: home page, category, shopping cart, and personal center. Each of them can be further divided into MVC+Session layer.

  • By project architecture

    The outermost layer is the Model, View, Controller, and Session layers, and the inner layer is the business module

    There is no need to say more about this one, just use the two together

1.1.2 Third-party libraries

Personal suggestion: If time permits, try to reinvent the wheel yourself. The risk is controllable and the maintenance is good.

If not necessary, try not to use the compiled third-party library (framework/.a) directly, and compile the third-party library yourself (safety requirement)

There are three ways to manage:

  • Manual management

    Manually maintain various third-party libraries, suitable for third-party libraries that have become stable and have few bugs

  • CocoaPods

  • Carthage

Carthage is recommended here because it is the least invasive to the project and is decentralized management, so there is no need to wait for a long pod update/install process. However, each has its own advantages. Using CocoaPods is simple and crude, and basically does not require any additional settings. It depends on your needs.

1.2 Project Architecture

When the project logic is basically centered around a main line, we can use MVC to meet our needs well. However, when the business logic becomes increasingly complex, we can no longer separate the business logic from the code by simply using the Model View Controller programming model, which is called decoupling.

In order to better decouple ViewController, the Model View ViewModel programming mode was created. The ViewModel layer actually bridges the Model and ViewController. This mode has advantages and disadvantages. This mode will generate a lot of glue code, but with the responsive programming framework (such as ReactiveCocoa or RxSwift), it can achieve the highest degree of decoupling. A good programming mode is the one that suits the complexity of your actual project business.

Extension: <About component-based programming>

If the project business is very complex and many business components are common, component-based programming can be used. A common approach is to use CocoaPods to split the project business modules into various pod libraries. You can directly integrate any module you want to use. Combined with MVVM and a responsive programming framework (such as ReactiveCocoa or RxSwift), you can achieve the highest degree of decoupling.

2. Crash & Performance Tuning

When the project has completed the business module and launched it, we can start to consider how to improve the user experience of the App. Here are a few examples:

1. Coding standards, regular code review?

2. Can the FPS be maintained at around 60 frames when scrolling a complex list?

3. Can the time spent on page loading and rendering be further reduced?

4. Is network caching done? Are the commonly used static resources of UIWebView/WKWebView cached?

5. Can the startup time of the App be reduced while maintaining minimal business logic?

2.1 UITest & UnitTest

When new requirements are developed, we first write UITest and UnitTest before testing to cover the main business process, which can improve the quality of our testing and reduce some visible bugs. In addition, with smoke test cases, the quality of our testing can be improved to the greatest extent (becoming the king of KPI - ????). Moreover, after going online, these unit tests and UITest component scripts can be used in conjunction with automated tests for regular regression testing to improve the quality of the App and reduce the crash rate.

2.2 NullSafe

In most cases, when we send a message to an NSNull object, a crash will occur. NSNull objects are common in the data returned by the background, and there may be null fields. Many JSON libraries will convert them into NSNull objects. The following situations will cause a crash:

  1. id obj = [NSNull null ];
  2. NSLog(@ "%@" , [obj stringValue]);

However, sending a message to a nil object will not cause a crash. For these, you can refer to the processing method in NullSafe and rewrite it.

The two methods - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector and - (void)forwardInvocation:(NSInvocation *)anInvocation forward the method signature that is unable to handle the message to a nil object without causing a crash.

In addition, common crashes such as NSArray value out of bounds and NSDictionary passing a nil object can be solved by using Method Swizzle in Runtime to hook the native method, as follows:

  1. @implementation NSMutableDictionary (NullSafe)
  2.   
  3. - ( void )swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector
  4. {
  5. Class class = [self class ];
  6.   
  7. Method originalMethod = class_getInstanceMethod( class , origSelector);
  8. Method swizzledMethod = class_getInstanceMethod( class , newSelector);
  9.   
  10. BOOL didAddMethod = class_addMethod( class ,
  11. origSelector,
  12. method_getImplementation(swizzledMethod),
  13. method_getTypeEncoding(swizzledMethod));
  14. if (didAddMethod) {
  15. class_replaceMethod( class ,
  16. newSelector,
  17. method_getImplementation(originalMethod),
  18. method_getTypeEncoding(originalMethod));
  19. } else {
  20. method_exchangeImplementations(originalMethod, swizzledMethod);
  21. }
  22. }
  23.   
  24. + ( void )load {
  25. static dispatch_once_t onceToken;
  26. dispatch_once(&onceToken, ^{
  27. id obj = [[self alloc] init];
  28. [obj swizzleMethod:@selector(setObject:forKey:) withMethod:@selector(safe_setObject:forKey:)];
  29.   
  30. });
  31.   
  32. }
  33.   
  34. - ( void )safe_setObject:(id)value forKey:(NSString *)key {
  35. if (value) {
  36. [self safe_setObject:value forKey:key];
  37. } else {
  38. NullSafeLogFormatter(@ "[NSMutableDictionary setObject: forKey:], Object cannot be nil" )
  39. }
  40. }
  41.   
  42. @end

This solution can avoid errors such as array value out of bounds, dictionary passing null values, removeObjectAtIndex, etc. The following crash can be avoided:

  1. id obj = nil;
  2. NSMutableDictionary *m_dict = [NSMutableDictionary dictionary];
  3. [dict setObject:obj forKey:@ "666" ];

2.2 Monitoring System

Currently, most apps integrate third-party statistics libraries, such as Tencent's Bugly, U-App from Umeng, etc. Here we introduce how to build your own performance monitoring library.

You can use PLCrashReporter or KSCrash library to parse the crash log and symbolize it, then upload it to the backend, collect and count it yourself. By the way, we used PLCrashReporter and Laravel in the backend, which made it very convenient to develop a simple crash and various performance parameter collection system, so if you want to build it yourself, you can consider this combination.

  • CPU, memory, FPS recording and saving

1. There are ready-made methods to obtain the three parameters of `CPU`, `FPS`, and `Memory usage` on the Internet. These three parameters belong to performance monitoring and can be recorded regularly, such as recording once every 10 seconds to a local file, and uploading yesterday's log every time you open the App. This requires you to formulate a log upload strategy yourself.
  • Caton log collection

    The lag that users can feel is usually caused by time-consuming operations in the main thread. Here are a few examples of lag:

  1. Initialize 10,000 UILabel instances in a for loop in viewDidLoad

  2. Manual sleep usleep(100*1000) in cellForRow proxy method

How to listen to these events? Check the source code, the simplified logic of the core method CFRunLoopRun is as follows:

  1. int32_t __CFRunLoopRun()
  2. {
  3. //Notify that runloop is about to enter  
  4. __CFRunLoopDoObservers(KCFRunLoopEntry);
  5.   
  6. do  
  7. {
  8. // Notify that timer and source will be processed  
  9. __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
  10. __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
  11.   
  12. __CFRunLoopDoBlocks(); //Handle non-delayed main thread calls  
  13. __CFRunLoopDoSource0(); //Handle UIEvent events  
  14.   
  15. //GCD dispatch main queue  
  16. CheckIfExistMessagesInMainDispatchQueue();
  17.   
  18. // About to enter sleep mode  
  19. __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
  20.   
  21. // Wait for kernel mach_msg event  
  22. mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
  23.   
  24. // Zzz...  
  25.   
  26. // wake up from waiting  
  27. __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
  28.   
  29. // Handle wakeup due to timer  
  30. if (wakeUpPort == timerPort)
  31. __CFRunLoopDoTimers();
  32.   
  33. //Handle asynchronous method wakeup, such as dispatch_async  
  34. else   if (wakeUpPort == mainDispatchQueuePort)
  35. __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
  36.   
  37. // UI refresh, animation display  
  38. else  
  39. __CFRunLoopDoSource1();
  40.   
  41. // Make sure again whether there is a synchronized method that needs to be called  
  42. __CFRunLoopDoBlocks();
  43.   
  44. } while (!stop && !timeout);
  45.   
  46. //Notify that the runloop is about to exit  
  47. __CFRunLoopDoObservers(CFRunLoopExit);
  48. }

We can see that if the waiting time in kCFRunLoopBeforeSources and kCFRunLoopBeforeWaiting is too long, it can be judged as a jam. What is specifically considered a jam? We all know that FPS is about 60 frames per second***, FPS is Frames Per Second. Strictly speaking, 60 frames per second is considered smooth, that is, one frame takes 1s/60 = 16.6ms. Considering the influence of some other events, several consecutive 50ms or a single time consumption is too long can be judged as a jam. After judging it as a jam, we can use PLCrashReporter or KSCrash to generate log records, which can be stored locally

We can use CFRunLoopObserverRef to obtain the changes of NSRunLoop status value in real time. The following is an example:

  1. @interfaceLagCollectionTool ( )
  2. {
  3. int timeoutCount;
  4. CFRunLoopObserverRef observer;
  5. BOOL observeLag;
  6. @ public  
  7. dispatch_semaphore_t semaphore;
  8. CFRunLoopActivity activity;
  9. }
  10.   
  11. @end
  12.   
  13.   
  14. @implementation LagCollectionTool
  15.   
  16. + (instancetype)shareInstance {
  17. static dispatch_once_t onceToken;
  18. static LagCollectionTool *tool = nil;
  19. dispatch_once(&onceToken, ^{
  20. tool = [[LagCollectionTool alloc] init];
  21. });
  22. return tool;
  23. }
  24.   
  25. - ( void )lanuch {
  26. if (observer)
  27. return ;
  28.   
  29. // Signal  
  30. semaphore = dispatch_semaphore_create(0);
  31.   
  32. // Register RunLoop status observation  
  33. CFRunLoopObserverContext context = {0,(__bridge void *)self,NULL,NULL};
  34. observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
  35. kCFRunLoopAllActivities,
  36. YES,
  37. 0,
  38. &runLoopObserverCallBack,
  39. &context);
  40. CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
  41.   
  42. //Monitor duration in child thread  
  43. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  44. while (YES)
  45. {
  46. long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
  47. if (st != 0)
  48. {
  49. if (!observer)
  50. {
  51. timeoutCount = 0;
  52. semaphore = 0;
  53. activity = 0;
  54. return ;
  55. }
  56.   
  57. if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
  58. {
  59. timeoutCount++;
  60. // NSLog(@"%d", timeoutCount);  
  61. if (timeoutCount < 5)
  62. continue ;
  63. NSLog(@ "----------------The card exploded!----------------" );
  64.   
  65. PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
  66. symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
  67. PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
  68.   
  69. NSData *data = [crashReporter generateLiveReport];
  70. PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
  71. NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
  72. withTextFormat:PLCrashReportTextFormatiOS];
  73. //Upload the log file  
  74.   
  75. }
  76. }
  77. timeoutCount = 0;
  78. }
  79. });
  80.   
  81. }
  82.   
  83. static   void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
  84. {
  85. LagCollectionTool *tool = (__bridge LagCollectionTool *)info;
  86. tool->activity = activity;
  87.   
  88. dispatch_semaphore_t semaphore = tool->semaphore;
  89. dispatch_semaphore_signal(semaphore);
  90. }

Crash gracefully and upload crash logs

By using NSSetUncaughtExceptionHandler to register your own exception handling callback, you can make the program appear calmer when a crash occurs, instead of directly crashing. You can pop up your own crash exception interface, which you can refer to Bilibili's interface. For example, if you encounter a high-energy reaction ahead, the program needs to be restarted, etc., so that users will not feel that the crash is abrupt. You can also manually maintain the Runloop after receiving the crash log. The following is a sample:

  1. // 1. Register ExceptionHandler  
  2.   
  3. + ( void )installUncaughtExceptionHandler {
  4. NSSetUncaughtExceptionHandler(&HandleException);
  5.   
  6. signal(SIGHUP, SignalHandler);
  7. signal(SIGINT, SignalHandler);
  8. signal(SIGQUIT, SignalHandler);
  9.   
  10. signal(SIGABRT, SignalHandler);
  11. signal(SIGILL, SignalHandler);
  12. signal(SIGSEGV, SignalHandler);
  13. signal(SIGFPE, SignalHandler);
  14. signal(SIGBUS, SignalHandler);
  15. signal(SIGPIPE, SignalHandler);
  16. }
  17.   
  18. // 2. Process crash information  
  19. void SignalHandler( int signal) {
  20. // 1. Get the call stack  
  21. // 2. Handle exceptions  
  22.   
  23. // 3. Keep App alive  
  24. BOOL isContiune = TRUE; // Whether to keep alive  
  25. CFRunLoopRef runLoop = CFRunLoopGetCurrent();
  26. CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
  27.   
  28. while (isContinuous) {
  29. for (NSString *mode in (__bridge NSArray *)allModes) {
  30. CFRunLoopRunInMode((CFStringRef)mode, 0.001, true );
  31. }
  32. }
  33. CFRelease(allModes);
  34. signal(SIGABRT, SIG_DFL);
  35. signal(SIGILL, SIG_DFL);
  36. signal(SIGSEGV, SIG_DFL);
  37. signal(SIGFPE, SIG_DFL);
  38. signal(SIGBUS, SIG_DFL);
  39. signal(SIGPIPE, SIG_DFL);
  40. }

extend:

The monitoring system is not limited to performance and crash rate, but can also extend statistical strategies to network request connectivity rate or some business levels to better control the quality of the App.

2.3 Performance Tuning & App Experience Optimization

Earlier, we introduced how to effectively reduce crashes and handle crashes gracefully. Now let’s take a look at a few points to pay attention to when solving performance problems.

2.3.1 Advantages and Disadvantages of Lazy Loading

Lazy loading is suitable for some pages that may not be loaded, such as pop-up boxes, empty data pages, etc. If used properly, it can avoid memory explosion. If used improperly, such as using lazy loading in a page that is bound to pop up, it may increase the page response time. Therefore, when using lazy loading, you must pay attention to the usage scenario to avoid side effects.

2.3.2 Avoid using repaint

Overriding the drawRect or drawReact:inContext method will create a layer context by default. The memory required for the graphics context is layer width * layer height * 4 bytes. Each time the layer is redrawn, the memory needs to be erased and reallocated, which will incur huge performance overhead.

The UIView class is actually an encapsulation of CALayer. There are many things about UI-level performance optimization. You can read the chapter about layer performance in iOS CoreAnimation Advanced Programming.

2.3.3 App experience optimization

When it comes to App experience optimization, it is actually a metaphysics. You need to find a balance between performance and experience. Common bad experiences include:

  • Improper use of UITableViewCell causes sliding lag

  • Performance issues caused by off-screen rendering when using a large number of cornerRadius and maskToBounds together

  • The network request operation does not have any status display, such as loading box, grayed-out button, etc.

  • Network requests are not cached

These problems are just the details of the App, but starting from the details can make it more professional~

Let's focus on network request optimization:

2.3.3.1 Manually maintain DNS resolution

Take www.manoboo.com as an example. When accessing through a domain name, the DNS resolution server will be searched first, and then it will be mapped to the IP of your own server. We can directly use the IP request interface to access network resources, which can avoid many problems, but there are pros and cons. You need to maintain the DNS mapping yourself. For example:

  1. Operator DNS traffic hijacking, specifically, your H5 webpage is inexplicably added with advertisements (for this problem, you can also make a domain name whitelist, prohibit requests for non-domain resources, or H5 handle it).

  2. DNS service providers (such as HiChina) resolution failures cause a large number of users to be unable to use the App normally, calculated on a daily basis.

  3. Loading timeouts caused by high DNS resolution latency lead to poor user experience

At this point, we can consider doing DNS resolution manually. To make it simple, we can replace the domain name in the URL when making a network request, or implement the corresponding method of the subclass of NSURLProtocol (URLProtocol in Swift) in Objective-C to replace the URL globally.

However, there are some disadvantages:

  1. The DNS resolution table needs to be maintained manually. When the resolution fails, a fault-tolerant solution is needed to ensure smooth operation of the interface.

  2. HTTP requests can be made by setting the host field in the header. HTTPS requests require additional configuration. Due to space limitations, detailed disadvantages and solutions can be found in the article HTTPDNS Practice in iOS.

2.3.3.2 Network request cache optimization

Applicable scenarios: Some scenarios with low update frequency: such as personal center

Regarding network request caching, the network requests from the App side are more about adding, deleting, modifying and querying to the backend. This aspect requires cooperation with the backend. Whether the resource changes, that is, whether the backend needs to re-retrieve or modify the data, at this time we need a value such as the timestamp Last-Modified or the identifier ETag to inform the server of its current resource tag. The commonly used strategies are:

Take the timestamp Last-Modified as an example

  1. The App requests the API for the first time, and the server returns a success response with HTTP Status 200. In the returned Header, Last-Modified is used to indicate the time when the resource was last modified on the server.

  2. The App requests the interface for the second time, and passes the Last-Modified in the local cache in the Header. If the resource on the server has not changed, the HTTP Status will be returned as 304. We can directly use the local cache, which reduces the transmission traffic and shortens the user's waiting time.

Note:

Quantification rather than guesswork is a principle in our development process. When we encounter performance problems, we can use instruments to measure various parameters in the actual operation process and find the problem (it is recommended to debug on a real machine instead of a simulator, as a real machine can better restore performance problems)

The tools in instruments have their own uses. For example, you can use Leask to check for memory leaks during the app's operation, use TimeProfiler to check the app startup time or method time, or you can be lazy and use the difference between CACurrentMediaTime() to calculate the method time.

Conclusion

Due to space limitations, some points are also generalized. How to optimize a project in iOS is a very deep subject, and the knowledge points are very wide. I have only touched upon a part of it. Learning never ends. While completing work, we can also be a cool programmer, learn Haskell to experience the fun of functional programming thinking, or use LLDB to be a better debugger.

***, thank you very much for reading this article. If my article is helpful, you can give me a little red heart??, welcome to my website www.manoboo.com to give me some feedback, I will try my best to create better articles

The articles cited in this article are as follows:

CocoaChina - iOS real-time freeze monitoring

iOS Core Animation: Advanced Techniques

The open source libraries involved in this article are as follows:

PLCrashReporter

KSCrash

MBNullSafe NullSafe library written by ManoBoo will further expand the functionality

<<:  Let's talk about APP push and analyze how each end collaborates to complete the push task

>>:  Four advantages and five applications of machine learning in the financial field

Recommend

A complete 5-step program for user growth!

This article’s 95-point growth plan is mainly div...

Is it true that food nowadays is not as delicious as before?

In the past, when food resources were scarce, it ...

Your analysis may not be correct. Logical reasoning has many traps.

[[149777]] People like to use a single chain of l...

9 examples of obtaining seed users

I often joke with my friends: Young people, don’t...

Time is getting faster and faster? It's not an illusion, it's a fact...

Review expert: Qian Hang, aerospace science exper...