background

The initialization logic hosted by Application#onCreate() becomes more and more complex as the company iterates on project requirements and projects rely on more libraries.

Take the initialization logic of last year’s online project as an example.

@Override
public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this))
        return;
    if (YXFDebuggable.strictMode()) {
        StrictModeUtil.startStrictMode();
    }
    AppDirs.init(app, PACKAGE_NAME);        	// dirsYXFStorage.init(app); // storage YXFLogs.init(app, YXFDebuggable.log(), YXFBuildInfo.getBuildStamp()); // logs YXFBuildInfo.init(app); YXFDeviceInfo.init(app); // device & build info YXFReceivers.init(app); GLLogManager.init(app); CrashHandler.getInstance().init(app); //crash YXCrashManager.init(app, new Bugrpt163Capture()); // skin ServiceManager.register(app, SkinGuideImpl.class); ServiceManager.register(app, SkinServiceImpl.class); //rxjavaplugins GodlikeRxPlugins.init(); // omit 200 lines of logic... }Copy the code

The project initialization code is really smelly and long… In the first project reconstruction, we try to split the initialization logic, divide all the original initialization logic into synchronous initialization and asynchronous initialization, combine the relatively aggregated logic into a Task, and the Task tasks can depend on each other. The code is logically divided into three methods to host Task initialization.

  • OnCreateBlock (), which ensures that all tasks are executing on the main threadApplication#onCreate()Completed before
  • OnCreateSync (), to ensure that execution is complete on the main thread, is usedhandle#post()Sending tasks to the main thread message queue queue for execution
  • OnCreateAsync () completes all Task execution on the child thread

The code logic is roughly as follows.

@Override
public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
        return;
    }
    onCreateBlock(this);
    onCreateSync(this);
    onCreateAsync(this);
}
private void onCreateBlock(Application app) {
    //task running
}
private void onCreateSync(Application app) {
    //post task to looper
}
private void onCreateAsync(final Application app) {
      new Thread(new Runnable() {
        @Override
        public void run() {
        	//task running
        }).start();
}
Copy the code

The task in onCreateBlock() is serial initialization, which greatly affects the timing of applying the first frame display. If onCreateBlock() is taking too long, you might want to rethink the code and redesign it. OnCreateSync () has no effect on the timing of the first frame display, while onCreateAsync() does not.

If no task category initialization is performed, all tasks must be initialized before the first frame is displayed

After classification, the original tasks are arranged as follows

The benefits of reducing initial blocking time are as follows

However, in a real scenario, there may be dependencies between the tasks in the split three methods, which makes the situation complicated:

  1. OnCreateBlock () can only rely on onCreateAsync(). M relying on onCreateSync() will cause a deadlock;
  2. OnCreateSync () can rely on tasks within the other two methods;
  3. OnCreateAsync () can rely on tasks within the other two methods while supporting as many threaded tasks as possible to speed up the completion time of all asynchronous tasks, depending on the CPU state of the current device

If the above logic is used to reorganize the initialization of the start task, the following logic needs to be implemented:

  1. The encapsulated Task supports running in the above three scenarios
  2. Thread pools are provided to run asynchronous tasks, and Handler#Post to run synchronous non-blocking tasks
  3. Using Task as the graph node, build an application dependency startup diagram and initialize it from the header
  4. Task running status interception provides external service logic to obtain state and interrupt initialization
  5. Simultaneous concurrency is maintained as much as possible when multiple different step chains are running
  6. .

Want to complete the above function, give priority to existing wheels. So I found Alpha on Github. Alpha is an Open source Android asynchronous startup framework built on PERT charts by Alibaba, which helps to correctly perform dependency tasks when an application is launched. After the integration, I found that it could not meet the application scenarios of the project. At that time, there was no good solution. Forced by the requirements of the project, I clone the source code to study the implementation, feeling a little lost, but also found the direction of optimization.

The defect of alpha

  1. The startup node granularity is not fine enough

    Alpha encapsulates the Task class for representing an asynchronous Task, deriving Project and AnchorTask for handling graph structures composed of multiple Task objects. The outer business only needs to inherit Task or the same build Project to write the business dependency logic. The ideal structure should be as follows

    But if you add a Task when it starts, it will receivexxxTask cannot be cast to com.alibaba.android.alpha.Project. From the source layer, you really can’t start from a task, which lacks flexibility.

  2. Unable to satisfy coasynchronous initialization dependency and blocks Application#onCreate

    Alpha is positioned as an asynchronous startup framework. When a startup task is executed, determine if it is executed by the main thread. If so, send it to the queue via handler#post, otherwise hand it to the thread pool. After the task is processed, the dependency check is performed on the tasks that depend on the task. If all dependencies on the task are completed, the task is started.

    Define a task T, whose start time is tTStart and end time is tTEnd. If any of the following dependent tasks exist,

    D (asynchronous) -> C (asynchronous) ->B (synchronous) ->A (synchronous)Copy the code

    Then tAStart < tAEnd < tBStart < tBEnd < tCStart < tCEnd < tDStart < tDEnd is always satisfied in alpha. Because tasks are queued when synchronized, their execution is not strictly synchronized with the context of the code block. When strict code synchronization is required in Application#onCreate(), for example

    public void onCreate(){startInitTask () // start the above chain code // post code block}Copy the code

    The post-code block is executed first. TCode < txStart (x = {A,B,C,D})

    Although AlphaManager#waitUntilFinish is provided in alpha to block the thread of execution, there are major drawbacks:

    If you wait in the UI thread, a deadlock will occur. The reason for this is that the executing code is waiting to be unlocked, and can only be unlocked when all tasks in the main thread are completed. The task is posted to the message queue, and can only be executed after being unlocked.

  3. Lack of synchronization support for task execution, co-asynchronous mixed chain support and scheduling optimization

    Many applications ensure that some initialization task is complete before entering the activity life cycle. At the same time, this precious time before the activity life cycle can be combined with the CPU resources of the device to perform as many asynchronous initialization tasks as possible.

Unfortunately, the last official update was two years ago, and there was no proper effort to support and maintain the Alpha library.

Anchors are better suited to the Android startup scenario

Anchors are a graph-based structure that allows asynchronously dependent task initialization of the Android startup framework. The anchor point feature provides the function of “hook” dependency, which can flexibly solve the complex synchronization problem during initialization. Refer to Alpha and improve some of its details to better fit the Android startup scenario, and support optimization of the dependency initialization process to choose a better path for initialization.

It has been serving our online project stably for more than a year. After transformation, it has advantages over Alpha

  1. The supporting configurationanchorsWaiting for a task chain, often usedapplication#onCreateEnsure that some initialization tasks are completed before enteringactivityLifecycle callback.
  2. Support active request blocking waiting tasks, often used for some initialization tasks on the task chain require logical confirmation by users;
  3. Support the same asynchronous task chain, so that your dependency chain is no longer limited to synchronous or asynchronous, flexible framework to help you switch scheduling.

If a task is guaranteed to complete before application#onCreate, it becomes an anchor task and is very simple to use.

Add the dependent

implementation 'com. Effective. The android: anchors: 1.1.0'
Copy the code

Start the dependency graph in Application and provide Java/Kotlin apis for easy use.

/ / Java code AnchorsManager. GetInstance () debuggable (true). AddAnchors (anchorYouNeed) // Pass the task ID to set the anchor task. Start (dependencyGraphHead); // Kotlin code getInstance().debuggable {true}.taskFactory {TestTaskFactory()} // Optionally, you can build dependency diagrams using factories,. Anchors {arrayOf()"TASK_9"."TASK_10"} // Pass the task ID to set the anchor point task.block("TASK_13") {// Optional, if you want users to interact after a task is completed, use the block function //According to Business it.smash() or it.unlock()}.graphics {// Optional, DSL build diagrams are supported when using projects, Sons (task_10.sons (task_11.sons (task_12.sons (TASK_13))), TASK_20.sons( TASK_21.sons( TASK_22.sons(TASK_23))), UITHREAD_TASK_B.alsoParents(TASK_22), UITHREAD_TASK_C ) arrayOf(UITHREAD_TASK_A) } .startUp()Copy the code

If you enable debug mode, you can see detailed information about the entire initialization process, which can be obtained from different tags.

  • TASK_DETAIL Task execution information

    D/TASK_DETAIL: TASK_DETAIL = = = = = = = = = = = = = = = = = = = = = = = task (UITHREAD_TASK_A) = = = = = = = = = = = = = = = = = = = = = = = | dependent tasks: whether | anchor task:false| thread information: the main | start time: 1552889985401 ms | wait for running time: 85 ms | run task time: 200 ms | end: 1552889985686 = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =Copy the code
  • ANCHOR_DETAIL Anchor task information

     W/ANCHOR_DETAIL: anchor "TASK_100" no found !
     W/ANCHOR_DETAIL: anchor "TASK_E"no found ! D/ANCHOR_DETAIL: Has some anchors! ("TASK_93") D/ANCHOR_DETAIL: TASK_DETAIL = = = = = = = = = = = = = = = = = = = = = = = task (TASK_93) = = = = = = = = = = = = = = = = = = = = = = = | dependent tasks: Whether TASK_92 | anchor task:true| Thread information: Anchors the Thread# 7| | start time: 1552891353984 ms time waiting to be run: 4 ms | run task time: 200 ms | end: 1552891354188 = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = D/ANCHOR_DETAIL: All anchors were released!Copy the code
  • LOCK_DETAIL Blocks wait information

     D/LOCK_DETAIL: Anchors Thread #9- lock( TASK_10 )
     D/LOCK_DETAIL: main- unlock( TASK_10 )
     D/LOCK_DETAIL: Continue the task chain...
    Copy the code
  • DEPENDENCE_DETAIL Information about the dependency diagram

     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_9_start(1552890473721) --> TASK_90 --> TASK_91 --> PROJECT_9_end(1552890473721)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_9_start(1552890473721) --> TASK_90 --> TASK_92 --> TASK_93 --> PROJECT_9_end(1552890473721)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_8_start(1552890473721) --> TASK_80 --> TASK_81 --> PROJECT_8_end(1552890473721)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_8_start(1552890473721) --> TASK_80 --> TASK_82 --> TASK_83 --> PROJECT_8_end(1552890473721)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_7_start(1552890473720) --> TASK_70 --> TASK_71 --> PROJECT_7_end(1552890473720)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_7_start(1552890473720) --> TASK_70 --> TASK_72 --> TASK_73 --> PROJECT_7_end(1552890473720)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_6_start(1552890473720) --> TASK_60 --> TASK_61 --> PROJECT_6_end(1552890473720)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_6_start(1552890473720) --> TASK_60 --> TASK_62 --> TASK_63 --> PROJECT_6_end(1552890473720)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_5_start(1552890473720) --> TASK_50 --> TASK_51 --> PROJECT_5_end(1552890473720)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_5_start(1552890473720) --> TASK_50 --> TASK_52 --> TASK_53 --> PROJECT_5_end(1552890473720)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_4_start(1552890473720) --> TASK_40 --> TASK_41 --> TASK_42 --> TASK_43 --> PROJECT_4_end(1552890473720)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_3_start(1552890473720) --> TASK_30 --> TASK_31 --> TASK_32 --> TASK_33 --> PROJECT_3_end(1552890473720)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_2_start(1552890473719) --> TASK_20 --> TASK_21 --> TASK_22 --> TASK_23 --> PROJECT_2_end(1552890473719)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> PROJECT_1_start(1552890473719) --> TASK_10 --> TASK_11 --> TASK_12 --> TASK_13 --> PROJECT_1_end(1552890473719)
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> UITHREAD_TASK_B
     D/DEPENDENCE_DETAIL: UITHREAD_TASK_A --> UITHREAD_TASK_C
    Copy the code

For both anchor and anchor scenarios, Trace Trace for each dependent task. Python systrace. Py can output trace.html for time analysis.

The dependency diagram has a dependency UITHREAD_TASK_A -> TASK_90 -> TASK_92 -> Task_93. Assuming that our dependency path is a prerequisite for subsequent business, we need to wait for that business to complete before we do our own business code. If not, we don’t care when they end. When using the anchor function, we check TASK_93, and the priority from the anchor to the anchor is increased. When priority CPU resources are available, the chain with the highest priority is used to preempt CPU resources. You can see from the figure above that the time to execute the dependency chain is shortened.

The Anchors project also provides the core Sample scenario to demonstrate.

  1. Multi-process initialization
  2. A middle node of the initialization chain waits for a response
  3. A new chain may be started after an initialization chain is complete

For example, if a middle node of the initialization chain needs to wait for a response, the business logic can decide whether to terminate the chain to continue initialization, etc., click Continue execution as follows.

The Log message can see that the block is unblocked.

D/LOCK_DETAIL: main- unlock( TASK_10 ) D/LOCK_DETAIL: Continue the task chain... D/Anchors: TASK_10_waiter -- onFinish -- D/TASK_DETAIL: TASK_DETAIL = = = = = = = = = = = = = = = = = = = = = = = task (TASK_10_waiter) = = = = = = = = = = = = = = = = = = = = = = = | dependent tasks: whether | anchor task:false| Thread information: Anchors the Thread# 10| | start time: 1595319503047 ms time waiting to be run: 1 ms | run task time: 3653 ms | end: 1595319506701 ============================================== D/Anchors: TASK_10_waiter -- onRelease --Copy the code

The anchors framework has a high degree of freedom and can be used in conjunction with the asynchronous hybrid chain and anchor functionality to handle many complex initialization scenarios, but it is important to fully understand the threading scenario when using the functionality.

If you are still stuck with a messy startup task, or are upset with asynchronous task control, many anchors are for you 👏 👏 👏

If you have any questions about the startup scenario or comments and suggestions on the framework design, please feel free to leave a comment