Some time ago to do cold start optimization, just did not write blog for a long time, feel it is still necessary to record.

I. Routine operation

public class MainActivity extends Activity {

    private static final Handler sHandler = new Handler(Looper.getMainLooper());
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sHandler.postDelay(new Runnable() {
            @Override
            public void run() {// Page startup time initializationdoSomething(); }}, 200); }}Copy the code

The first thing most developers consider when initializing a page’s cold start time is the handler.postdelay () method. But how long is the right delay? 100 ms? 500 ms? Or 1 s?

Delay too late, there may be experience problems; Premature delay has no effect on cold start. The delay time (such as 200ms) is ok when tested on Samsung mobile phone, but there is a problem when tested on Huawei mobile phone. Then, we constantly adjust the delay time based on the adaptation of the model, trying to find the most appropriate value, but it is found that it is impossible.

2. Starting and ending points

Let’s start with a picture



Above, Google provides the flow chart of the cold start, you can see the cold start when the starting point for the Application. The onCreate () method, the end point in ActivityRecord. ReportLanuchTimeLocked () method.

You can view the cold startup time in the following two ways

1. Check the Logcat

The following logs are Displayed in The Displayed keyword of Android Studio Logcat:

The 2019-07-03 01:49:46. 748, 1678-1718 /? I/ActivityManager: Displayed com.tencent.qqmusic/.activity.AppStarterActivity: +12s449ms


12s449ms is the cold start time

2.adb dump

Adb shell am start -w -s < package name/full class name >



“ThisTime: 1370” is the cold startup time (unit: ms)

Find an effective end to the callback

Know above, cold start timing starting point is the Application. The onCreate (), the end point is ActivityRecord reportLanuchTimeLocked (), but this is not the place where we can write business logic, Most applications use Activity as a carrier, so where is the end callback?

1.IdleHandler

From the cold start flowchart, the end time is calculated after the UI renders, so it is obvious that onCreate(), onResume(), and onStart() in the Activity life cycle cannot be used as cold start end callbacks.

Handler. PostDelay (). The problem is that the Delay time is not fixed, but we know that MessageQueue has an ArrayList<IdleHandler>

public final class MessageQueue {

    Message mMessages;
    priavte final ArrayList<IdleHandler> mIdelHandlers = new ArrayList<IdelHandler>();
    
    Message next() {... int pendingIdelHandlerCount = -1; // -1 only during first iterationfor(;;) {... // If first time idle,then get the number of idlers to run.
            // Idle handles only run if the queue is empty or if the first message
            // in the queue (possibly a barrier) is due to be handled in the future.
            if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // No idle handlers to run.  Loop and wait some more.
                mBlocked = true;
                continue;
            }
            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];    
                mPendingIdleHandlers[i] = null;
                // release the reference to the handler
                boolean keep = false;
                try {        
                     keep = idler.queueIdle();    
                } catch (Throwable t) {        
                     Log.wtf(TAG, "IdleHandler threw exception", t); }}... }}}Copy the code

You can add Idle tasks to the list. Idle tasks will be executed only when the MessageQueue queue is empty. That is, tasks in the Idle thread will be executed only when the task in the MessageQueue queue has been executed.

During a cold start, place the time-consuming initialization task in Idle in activity.onCreate ()

public class MainActivity extends Activity {

    private static final Handler sHandler = new Handler(Looper.getMainLooper());
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
        @Override    
        public boolean queueIdle() {// Page startup time initializationdoSomething();
            return false; }}); }}Copy the code

Under normal circumstances, the initialization task is executed after all tasks of the UI thread are completed, and the model is not considered in this solution. But there’s a problem, what if the UI thread never completes its task? Can that happen? 🌰, for example, has a scrolling Banner at the top of the Activity home page. The scrolling of the Banner is implemented by increasing the delay Runnable. Then, the initialization task may never be executed.

In addition, if the initialization task involves a UI refresh, performing it after the Activity has been displayed may be a loss of experience.

Reviewing the flow chart of cold start, when the cold start ends, it is just after the UI rendering is finished. If we can ensure that the task is executed after the UI rendering is finished, we can not only improve the cold start data, but also solve the UI problem.

Therefore, the solution must be tied to the bell, to find the most appropriate end callback, or have to see the source code.

2.onWindowFocusChanged()

First of all, we found the first solution

public class BaseActivity extends Activity {

    private static final Handler sHandler = new Handler(Looper.getMainLooper());
    private boolean onCreateFlag;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        onCreateFlag = true;
        setContentView(R.layout.activity_main);
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {    
        super.onWindowFocusChanged(hasFocus);    
        if (onCreateFlag && hasFocus) {
            onCreateFlag = false;
            
            sHandler.post(new Runnable() {
                @Override
                public void run() {
                    onFullyDrawn();
                }
            })
        }
    }

    @CallSuper
    protected void onFullyDrawn() {
        // TODO your logic
    }
}Copy the code

Those interested in the system call flow for onWindowFocusChanged() can check out my last article
The Activity. OnWindowFocusChanged () call process”




As for why onWindowFocusChanged() was delayed by handler.post (), I started by clicking onWindowFocusChanged(). When there was no post(), onWindowFocusChanged() was Displayed before the Displayed Log. RequestLayout () will only trigger a rendering when the SurfaceFlinger signal returns, so it will delay a task just after it



3.View.post(Runnable runnable)

In the second case, we implement the view. post(Runnable Runnable) method

public class BaseActivity extends Activity {

    private static final Handler sHandler = new Handler(Looper.getMainLooper());
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main); } protected void postAfterFullDrawn(final Runnable Runnable) {if (runnable == null) {        
            return;    
        }    
        getWindow().getDecorView().post(new Runnable() {        
            @Override        
            public void run() { sHandler.post(runnable); }}); }}Copy the code

Note that this scheme only works if onResume() or before is called. Why is that?

View. Post () source code implementation

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    // Be careful here! AttachInfo is not empty and actually delays a task by handler.post ()
    if(attachInfo ! =null) {        
        return attachInfo.mHandler.post(action);    
    }

    // Postpone the runnable until we know on which thread it needs to run.    
    // Assume that the runnable will be successfully placed after attach.    
    getRunQueue().post(action);    
    return true;
}

private HandlerActionQueue mRunQueue;

private HandlerActionQueue getRunQueue(a) {    
    if (mRunQueue == null) {        
        mRunQueue = new HandlerActionQueue();    
    }    
    return mRunQueue;
}Copy the code

Handleractionqueue.post () is called with view.post ()

public class HandlerActionQueue { 
       
    private HandlerAction[] mActions;    
    private int mCount;    

    public void post(Runnable action) {        
        postDelayed(action, 0);    
    }    

    /** * This method simply stores the incoming task Runnable into an array **/
    public void postDelayed(Runnable action, long delayMillis) {        
        final HandlerAction handlerAction = new HandlerAction(action, delayMillis);        
        synchronized (this) {            
            if (mActions == null) {                
                mActions = new HandlerAction[4]; } mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction); mCount++; }}}Copy the code

At this point, we call view. post(Runnable Runnable) simply to store the task Runnable as a HandlerAction in the HandlerActionQueue’s HandlerAction[] array. So when does this array get called?

Since is a cold start, that still have to look at cold start system callback, directly see ActivityThread. HandleResumeActivity ()

final void handleResumeActivity(IBinder token,
    boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { ActivityClientRecord r = mActivities.get(token); . r = performResumeActivity(token, clearHide, reason); .if(r ! =null) {
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (r.mPreserveWindow) {    
                a.mWindowAdded = true;    
                r.mPreserveWindow = false;    
                ViewRootImpl impl = decor.getViewRootImpl();    
                if(impl ! =null) { impl.notifyChildRebuilt(); }}if (a.mVisibleFromClient) {    
                if(! a.mWindowAdded) { a.mWindowAdded =true;
                    // The next step is to render the viewrotimpl
                    wm.addView(decor, l);    
                } else {            
                    a.onWindowAttributesChanged(l);    
                }
            }
        }
    }
}Copy the code

To apply colours to a drawing, directly into the ViewRootImpl. PerformTraversals ()

public final class ViewRootImpl implements ViewParent.View.AttachInfo.Callbacks.ThreadedRenderer.DrawCallbacks {
    
    boolean mFirst;

    public ViewRootImpl(Context context, Display display) {... mFirst =true; // true for the first time the view is added. }private void performTraversals(a) {
        finalView host = mView; .if (mFirst) {
            ...
            host.dispatchAttachedToWindow(mAttachInfo, 0); . }... performMeasure(); performLayout(); preformDraw(); . mFirst =false; }}Copy the code

Go to View. The dispatchAttachedToWindow () to see

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    // Be careful when backing up! Mind your reversing! MAttachInfo here! = null!mAttachInfo = info; .// Transfer all pending runnables.
    // Execute the pending task runnbales
    if(mRunQueue ! =null) {    
        mRunQueue.executeActions(info.mHandler);    
        mRunQueue = null; }... }// Start accessing the previously stored tasks and see how executeActions() works
public class HandlerActionQueue {        
    private HandlerAction[] mActions;
    
    /** * Are you showing me this? It actually calls handler.post () to perform the task **/
    public void executeActions(Handler handler) {    
        synchronized (this) {        
            final HandlerAction[] actions = mActions;        
            for (int i = 0, count = mCount; i < count; i++) {            
                final HandlerAction handlerAction = actions[i];            
                handler.postDelayed(handlerAction.action, handlerAction.delay);
            }        
            mActions = null;        
            mCount = 0; }}}Copy the code

That is, the View maintains a HandlerActionQueue, and we can store the task Runnables in the HandlerActionQueue via view.post () before DecorView attachToWindow. When a DecorView attachToWindow is first iterated through the array of tasks previously stored in the HandlerActionQueue, one by one through the handler.

1. In the View. DispatchAttachedToWindow () mAttachInfo was assignment, and as a result, after the post () is called directly actual Handler. The post () to perform a task. Looking further back, performResumeActivity() is executed before rendering, which explains why only calls to onResume() or before are valid


2. Delay a task in the view.post () Runnable run() callback from performTraverals()
The order of calls looks like the next task executes after rendering

4. The neglected Theme

Let’s take a look at two renderings



After clicking the Icon on the desktop for the first time, the app did not pull up immediately, but paused, giving people the impression that the phone was stuck.

After clicking the desktop Icon in the second one, a blank screen appeared immediately, and after a period of time, the background image appeared, which clearly felt like an app card in the experience.

So what makes them different? The answer is to set the splash screen Activity theme to a full-screen transparent style with no title bar

<activity
	android:name="com.huison.test.MainActivity"
	.
	android:theme="@style/TranslucentTheme" />

<style name="TranslucentTheme" parent="android:Theme.Translucent.NoTitleBar.Fullscreen" />Copy the code

In this way, you can solve the problem of cold startup blank screen or black screen, and the experience will be better.

In live.

Cold start optimization, summed up in 12 words”
Subtraction is the main, asynchrony is the auxiliary, delay is the complement

Subtraction is given priority to

Do subtraction as far as possible, can not do as far as possible not to do!

Application.oncreate () must be light! Be light! Be light! There will be more or less access to third-party SDKS in the project, but don’t initialize them all in application.oncreate (). Try to load them lazily.

Debug packages can add log printing and partial statistics, but Release does not add them

Asynchronous is complementary

Make time-consuming tasks as asynchronous as possible! Many RDS do not like to do callbacks. When a status value is obtained, it is directly called even if the function is time-consuming. Asynchronous callback to refresh the state value can also meet business requirements.

Of course, not all scenarios use asynchronous callbacks, because asynchrony involves thread switching, and in some scenarios, it can flash, and the UI experience is terrible, so try!

Delay to fill

In fact, the previous end point is to pave the way for delay, but the delay scheme is not the best, when we delay the cold start task to the end, the cold start is solved, but there may be too many tasks at the end of the load and cause other problems, such as ANR, interaction lag. When I was on the server side, I had a guy on the front end (millions of DAUs at the time) who wrote a request to an interface at 9am and the interface called the alarm. If he changed it from 9am to 10am, the result would be the same, and then changed it to block random requests, which smoothen out the peak. Similarly, if the cold start process delays all tasks to the end point, the end point can also be overloaded.

Cutting peak fill valley, discrete task, reasonable use of computer resources is to solve the fundamental problem!

other

1. Cold starts minimize the use of SharedPreferences, especially with file operations, where the underlying ContextImpl synchronization lock often freezes directly. Some people online said to replace SP with wechat MMKV, I tried, the effect is not very obvious, maybe related to the project, but it takes time to initialize MMKV.


2. Pay attention to the resident memory and GC of cold startup. If GC is too frequent, it will also have an impact



Alipay client architecture analysis: Android client startup speed optimization “garbage Collection”

At this point, cold start optimization summary is also concluded, some people will ask do so much, the effect in the end? Seems to be oh, the most afraid is “a meal operation fierce as tiger, online review 250”! Gp-vitals has a cold start indicator, the percentage of long cold start time (>5s) before project optimization is 3.63%, and the percentage drops to 0.95% after one meal operation. Wow! Surprise!