The background,

Tomorrow is Tuesday, which happens to be the weekly technology sharing of our team. I will share a set of weird cold start speed optimization plan which I spent a few days ago while doing other work.

Second, deliberately declare

The content of my article will not involve the online change is the conventional optimization program ~, at the same time, usually work, work content is mixed and many, so this optimization program is not particularly mature, just for reference ~

Third, the most common optimization scheme

  • Lazy loading of data, such as not retrieving data when the Fragment user is not visible
  • Optimize the layout hierarchy to reduce the time spent on the first inflate layout
  • Most of the SDK’s initializations are run in the thread pool
  • If you can use the ViewStub, use it and load the layout on demand
  • Try to avoid the main thread during startup to unpack some global configuration data
  • It is not only the three-party library that can put child threads, but also some logic that is not so time-sensitive


Iv. Project structure

In our Android project, after the application has gone through the splash screen, it will enter the main screen – MainActivity. I have joked about this place many times, and the splash screen advertisement as a launcher is really not very reliable. The best way should be to start AdActivity from the MainActivity. You can even use a full-screen AdView instead of an Activity.

First, a brief introduction to the structure of MainActivity involved in our project:

It’s just a simple drawing. It’s like… The shame of drawing…

It’s ok to have a look at the meaning, I share in the group is to use this sketch, anxious to get off work, not to draw again.



When the App is cold started, there are too many visible things to initialize, and the Fragment itself is a relatively heavy thing. It’s much lighter than an Activity, but heavier than a View

There are about 4-5 tabs on our home page, and each TAB is a Fragment, and the first TAB has 4 fragments embedded in it. My optimization this time mainly aims at tab1 on the home page and the four tabs embedded in tab1

Fifth, the ultimate lazy loading

5.1 Extreme lazy loading

Usually see lazy load:

That is, when the fragment is initialized, it will be executed along with the network request we wrote, which is very costly. Ideally, the network request will only be performed when the user clicks on or slides into the current fragment. Hence the term lazy loading.


But…

Since the four sub-tabs on the first screen are inherited from a base class BaseLoadListFragment, the logic of data loading is very dead. According to the above modification method, the impact is too large. It could lead to more trouble later

5.2 Lazy Loading Scheme

  1. When the first screen is loaded, only the default TAB is inserted into the ViewPager, and the remaining TAB is replaced with an empty Fragment
  2. When a user slide to the other TAB, such as sliding dynamic TAB to friends, use FriendFragment EmptyPlaceholderFragment replace the current, then the adapter. NotifyDataSetChanged
  3. When all four tabs are replaced with data tabs, the reference to the EmptyFragment is removed to free the memory


Here, we have to mention a cliche of a hole, because our home page is implemented with the ViewPager + FragmentPagerAdapter. So there’s a pit:

When ViewPager and FragmentPagerAdapter are used together, notifyDataSetChanged() is invalid and cannot refresh the Fragment list

I’ll talk about this in more detail

5.3 FragmentPagerAdapter与FragmentStatePagerAdapter

When we use ViewPager to load fragments, we are officially provided with both adapters, which inherit from PagerAdapter.

The difference, on the official description:

FragmentPagerAdapter

This version of the pager is best for use when there are a handful of typically more static fragments to be paged through, such as a set of tabs. The fragment of each page the user visits will be kept in memory, though its view hierarchy may be destroyed when not visible. This can result in using a significant amount of memory since fragment instances can hold on to an arbitrary amount of state. For larger sets of pages, consider
FragmentStatePagerAdapter.


FragmentStatePagerAdapter

This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared to
FragmentPagerAdapter at the cost of potentially more overhead when switching between pages


Conclusion:

  • Using FragmentStatePagerAdapter, if the TAB is not visible for the users, the Fragment is destroyed, FragmentPagerAdapter does not, use FragmentPagerAdapter, All fragments on a TAB are held in memory
  • When the TAB is very long, it is recommended to use FragmentStatePagerAdapter
  • FragmentPagerAdapter is recommended when there are few or fixed tabs

This is the ViewPager+FragmentPagerAdapter used in our project.

5.4 FragmentPagerAdapter Refresh Faults

Normally, when we use Adapter, all we need to do to refresh data is:

  1. To update the dataSet
  2. Call notifyDataSetChanged ()

However, this is not applicable in this Adapter. Because (impatient of this step can directly see the summary of the following) :

  1. The default PagerAdapter destoryItem will only detach the Fragment, not remove it
  2. When instantiateItem is called again, the Fragment removed by detach will be removed from the mFragmentManager and attached again



The dataSetChanged code for ViewPager is as follows:



4, and adapter’s default implementation



A quick summary:

1, ViewPager dataSetChanged() uses adapter.getitemPosition to determine whether to remove the current Item (remove position = POSITION_NONE).

2. The getItemPosition of PagerAdapter is implemented POSITION_UNCHANGED by default

The above points result in the ViewPager not having the chance to call the Adapter’s instantiateItem once the Adapter has been built.

Furthermore, even if we overwrite the getItemPosition method and return POSITION_NONE each time, we will not replace the Fragment because in the instantiateItem method, The default implementation of getItemId() is return Position.


5.5 FragmentPagerAdapter Correct refresh posture

Overwrite getItemId() and getItemPosition()

 class TabsAdapter extends FragmentPagerAdapter {

        private ArrayList<Fragment> mFragmentList;
        private ArrayList<String> mPageTitleList;
        private int mCount;

        TabsAdapter(FragmentManager fm, ArrayList<Fragment> fragmentList, ArrayList<String> pageTitleList) {
            super(fm);
            mFragmentList = fragmentList;
            mCount = fragmentList.size();
            mPageTitleList = pageTitleList;
        }

        @Override
        public Fragment getItem(int position) {
            return mFragmentList.get(position);
        }

        @Override
        public CharSequence getPageTitle(int position) {
            return mPageTitleList.get(position);
        }

        @Override
        public int getCount() {
            returnmCount; } @override public long getItemId(int position) { Will continue to find fragments cached in the FragmentManagerreturnmFragmentList.get(position).hashCode(); } @override public int getItemPosition(@nonnull Object Object) {returnInt index = mfragmentList.indexof (object);if (index == -1) {
                return POSITION_NONE;
            } else {
                returnmFragmentList.indexOf(object); } } void refreshFragments(ArrayList<Fragment> fragmentList) { mFragmentList = fragmentList; notifyDataSetChanged(); }}Copy the code

Other relevant codes:

(1) to realize the ViewPager OnPageChangeListener, to monitor the ViewPager sliding condition, can only when moving to the next TAB for fragments replace operation, Where mDefaultTab is the serial number of the current TAB display returned by the interface

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    }

    @Override
    public void onPageSelected(int position) {
        mCurrentSelectedTab = position;
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        if(! hasReplacedAllEmptyFragments && mCurrentSelectedTab ! = mDefaultTab && state == 0) {// If: 1. 2. The current TAB is not the default TAB initialized (default TAB will not be replaced with an empty Fragment) 3. Slide over, namely the state = 0 replaceEmptyFragmentsIfNeed (mCurrentSelectedTab); }}Copy the code

Remark:

OnPageScrollStateChanged Connects to the status value of the slide. There are three values:

0: nothing is done 1: start to slide 2: end of slide

A slide that causes a page switch has the order of state: 1 -> 2 -> 0

(2) Replace the Fragment, because the number of tabs can be changed according to the global config information, so this place is a little bit confusing.

/** *return* Replacement process: * * @param tabId Specifies the tabId of the TAB to be replaced - (the current empty Fragment is in the adapter data list mFragmentList) */  private void replaceEmptyFragmentsIfNeed(int tabId) {if (hasReplacedAllEmptyFragments) {
            return; } int tabRealIndex = mEmptyFragmentList.indexOf(mFragmentList.get(tabId)); // Find the current empty Fragment in the mEmptyFragmentListif (tabRealIndex > -1) {
            if(Collections.replaceAll(mFragmentList, mEmptyFragmentList.get(tabRealIndex), mDataFragmentList.get(tabRealIndex))) { mTabsAdapter.refreshFragments(mFragmentList); // Refresh the data after replacing the corresponding Empty fragment in the mFragmentList. Boolean hasAllReplaced =true;
                for (Fragment fragment : mFragmentList) {
                    if (fragment instanceof EmptyPlaceHolderFragment) {
                        hasAllReplaced = false;
                        break; }}if(hasAllReplaced) { mEmptyFragmentList.clear(); / / replace all completed, release reference} hasReplacedAllEmptyFragments = hasAllReplaced; }}}Copy the code


6. Magical preloading (preloading views, not data)

Some View preloading schemes that may be involved in Android startup:

  1. Create a WebView in advance because it takes a long time to create a WebView. If the first screen has an H5 page, you can create a WebView in advance.
  2. The onCreate of the Application allows you to start working with the Layout inflate in the child thread, starting with the official AsyncLayoutInflater
  3. The preloading of data to populate the View, which is not covered today

6.1 What needs to be preloaded

This is the layout of the base class of the four Tab fragments on the home page. Due to the unreasonable design of something, the hierarchy is very deep. As a result, the three tabs on the home page plus the FeedMainFragment itself take a very long time to inflate the View inflate out. So we consider advancing the inflate Layout in child threads




6.2 modify AsyncLayoutInflater

There is officially a class that can be synchronized synchronized, but it has two drawbacks:

  1. We need a new one on the spot every time
  2. Asynchronously loaded views can only be retrieved via callback (dead end)


So you decide to encapsulate an AsyncInflateManager yourself, use thread pools internally, and have a caching mechanism for views that are inflate completed. The core LayoutInflater is simply copied.

First look at AsyncInflateManager implementation, here I directly copy code in, rather than screenshots, so that if you want to use some of the things, you can directly copy:

/** * @author zoutao * <p> * Is used to provide the function of a child thread inflate view, avoiding the fact that the view hierarchy is too deep and too complex, and that the main thread inflate view takes too long. */ Public class AsyncInflateManager {private static AsyncInflateManager sInstance; private ConcurrentHashMap<String, AsyncInflateItem> mInflateMap; InflateKey and InflateItem are saved, which contain any inflate task private ConcurrentHashMap<String, CountDownLatch> mInflateLatchMap; private ExecutorService mThreadPool; // The thread pool with which the inflate works is privateAsyncInflateManager() {
        mThreadPool = new ThreadPoolExecutor(4, 4, 0, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>());
        mInflateMap = new ConcurrentHashMap<>();
        mInflateLatchMap = new ConcurrentHashMap<>();
    }

    public static AsyncInflateManager getInstance() {singleton} /** * to get the view that is asynchronously synchronized out ** @param context * @param layoutResId Needs to take the layoutId * @param parent Container * @param inflateKey Every View has an inflateKey because you can have many layouts with the same inflate, InflateKey: @param inflater An inflater that is propagated externally, and if it has an inflater that is passed in, it is used to perform a possible SyncInflate, * @returnView */ @uithRead @nonNULL Public view getInflatedView(Context Context, int layoutResId, @Nullable ViewGroup parent, String inflateKey, @NonNull LayoutInflater inflater) {if(! TextUtils.isEmpty(inflateKey) && mInflateMap.containsKey(inflateKey)) { AsyncInflateItem item = mInflateMap.get(inflateKey); CountDownLatch latch = mInflateLatchMap.get(inflateKey);if(item ! = null) { View resultView = item.inflatedView;if(resultView ! = null) {return removeInflateKey(inflateKey); replaceContextForView(resultView, context);return resultView;
                }

                if(item.isInflating() && latch ! = null) {// The view is not synchronized, but in the inflate, wait to return try {latch.wait(); } catch (InterruptedException e) { Log.e(TAG, e.getMessage(), e); } removeInflateKey(inflateKey);if(resultView ! = null) { replaceContextForView(resultView, context);returnresultView; }} // If the inflate is not already started, set this tofalseThe UI thread is inflate item.setcancelled (true); }} // The View with the asynchronous inflate fails, the UI thread inflatereturn inflater.inflate(layoutResId, parent, false); } /** * Inflater initializes as an application passed in, and the context inflate out of the view cannot be used as startActivity, so wrap it with MutableContextWrapper. */ private void replaceContextForView(View inflatedView, Context Context) {if (inflatedView == null || context == null) {
            return;
        }
        Context cxt = inflatedView.getContext();
        if (cxt instanceof MutableContextWrapper) {
            ((MutableContextWrapper) cxt).setBaseContext(context);
        }
    }

    @UiThread
    private void asyncInflate(Context context, AsyncInflateItem item) {
        if (item == null || item.layoutResId == 0 || mInflateMap.containsKey(item.inflateKey) || item.isCancelled() || item.isInflating()) {
            return;
        }
        onAsyncInflateReady(item);
        inflateWithThreadPool(context, item);
    }

    private void onAsyncInflateReady(AsyncInflateItem item) {
       ...
    }

    private void onAsyncInflateStart(AsyncInflateItem item) {
        ...
    }

    private void onAsyncInflateEnd(AsyncInflateItem item, boolean success) {
        item.setInflating(false);
        CountDownLatch latch = mInflateLatchMap.get(item.inflateKey);
        if(latch ! = null) {// Release latch.countdown (); }... } private void removeInflateKey(String inflateKey) { ... } private void inflateWithThreadPool(Context context, AsyncInflateItem item) { mThreadPool.execute(newRunnable() {
            @Override
            public void run() {
                if(! item.isInflating() && ! item.isCancelled()) { try { onAsyncInflateStart(item); item.inflatedView = new BasicInflater(context).inflate(item.layoutResId, item.parent,false);
                        onAsyncInflateEnd(item, true);
                    } catch (RuntimeException e) {
                        Log.e(TAG, "Failed to inflate resource in the background! Retrying on the UI thread", e);
                        onAsyncInflateEnd(item, false); }}}}); } /** * copy from AsyncLayoutInflater - actual inflater */ private static class BasicInflater extends LayoutInflater { private static final String[] sClassPrefixList = new String[]{"android.widget."."android.webkit."."android.app."};

        BasicInflater(Context context) {
            super(context);
        }

        public LayoutInflater cloneInContext(Context newContext) {
            return new BasicInflater(newContext);
        }

        protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
            for (String prefix : sClassPrefixList) {
                try {
                    View view = this.createView(name, prefix, attrs);
                    if(view ! = null) {return view;
                    }
                } catch (ClassNotFoundException ignored) {
                }
            }
            returnsuper.onCreateView(name, attrs); }}}Copy the code


Here I use an AsyncInflateItem to manage a unit of the inflate at a time,

/** * @author zoutao */ public class AsyncInflateItem { String inflateKey; int layoutResId; ViewGroup parent; OnInflateFinishedCallback callback; View inflatedView; private boolean cancelled; private boolean inflating; // There are some moresetThe get method}Copy the code

And finally the inflate callback:

public interface OnInflateFinishedCallback {
    void onInflateFinished(AsyncInflateItem result);
}Copy the code


With this encapsulation, the outside can start the task of the asynchronous Synchronized View directly in the onCreate of the Application. Call as follows:

AsyncInflateUtil.startTask();Copy the code

/**
 * @author zoutao
 */
public class AsyncInflateUtil {
    public static void startTask() {
        Context context = new MutableContextWrapper(CommonContext.getApplication());
        AsyncInflateManager.getInstance().asyncInflateViews(context,
                new AsyncInflateItem(InflateKey.TAB_1_CONTAINER_FRAGMENT, R.layout.fragment_main),
                new AsyncInflateItem(InflateKey.SUB_TAB_1_FRAGMENT, R.layout.fragment_load_list),
                new AsyncInflateItem(InflateKey.SUB_TAB_2_FRAGMENT, R.layout.fragment_load_list),
                new AsyncInflateItem(InflateKey.SUB_TAB_3_FRAGMENT, R.layout.fragment_load_list),
                new AsyncInflateItem(InflateKey.SUB_TAB_4_FRAGMENT, R.layout.fragment_load_list));

    }

    public class InflateKey {
        public static final String TAB_1_CONTAINER_FRAGMENT = "tab1";
        public static final String SUB_TAB_1_FRAGMENT = "sub1";
        public static final String SUB_TAB_2_FRAGMENT = "sub2";
        public static final String SUB_TAB_3_FRAGMENT = "sub3";
        public static final String SUB_TAB_4_FRAGMENT = "sub4"; }}Copy the code



Note: there will be a pit. That is, in the onCreate of the Application, the only Context you can get is the Application, so the View inflate, the Context that the View holds is the Application, and that causes a problem.

If you use the view.getContext () context to jump to an Activity. Throw exceptions

Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?


If you want to pass in an Activity to create a LayoutInflater, it’s too late. Context is an abstract class, and the wrapper that implements it is ContextWrapper. Activity, Appcation, etc., are subclasses of ContextWrapper. However, ContextWrapper also has a magical subclass.

package android.content; /** * Special version of {@link ContextWrapper} that allows the base context to * be modified after it is initially set.  */ public class MutableContextWrapper extends ContextWrapper { public MutableContextWrapper(Context base) { super(base); } /** * Change the base contextfor this ContextWrapper. All calls will then be
     * delegated to the base context.  Unlike ContextWrapper, the base context
     * can be changed even after one is already set.
     * 
     * @param base The new base context for this wrapper.
     */
    public void setBaseContext(Context base) { mBase = base; }}Copy the code

6.3 Decorator mode

You can see that the Context design on Android uses the decorator pattern, which greatly improves flexibility. What impressed me most about this example is that when the official class MutableContextWrapper was not provided, we could have implemented it in the same way ourselves. Thinking must be flexible ~


Seven,

Common startup speed optimization schemes include:

  • Lazy loading of data, such as not retrieving data when the Fragment user is not visible
  • Optimize the layout hierarchy to reduce the time spent on the first inflate layout
  • Most of the SDK’s initializations are run in the thread pool
  • If you can use the ViewStub, use it and load the layout on demand
  • Try to avoid the main thread during startup to unpack some global configuration data
  • It is not only the three-party library that can put child threads, but also some logic that is not so time-sensitive

These can be found on the Internet a large number of articles as well as the implementation of each major scheme.

First of all, the general direction of optimization must be set first:

  1. Lazy loading
  2. preload

Lazy loading:

  • When the first screen is loaded, only the default TAB is inserted into the ViewPager, and the remaining TAB is replaced with an empty Fragment
  • When a user slide to the other TAB, such as sliding dynamic TAB to friends, use FriendFragment EmptyPlaceholderFragment replace the current, then the adapter. NotifyDataSetChanged
  • When all four tabs are replaced with data tabs, the reference to the EmptyFragment is removed to free the memory

Preload:

  • In the Application onCreate method, the Layout is inflate out first in the child thread for any subsequent fragments
  • Add a cache access mechanism for views that are synchronized, as well as a wait mechanism
    • If the inflate is in, the block wait is performed
    • If the inflate is completed, extract the View and release the cache’s reference to the view
    • The Inflate is created directly in the UI thread if the Inflate has not already been started

These schemes may not work very well, so they are for reference only

We learned ↓ from the design of ContextWrapper and MutableContextWrapper

When writing code, design first and choose the most appropriate design pattern, and the subsequent earnings far outweigh the time and mental cost of writing a document and coming up with a design







My Jane booksZou Ah Tao Tao Tao’s brief book

My CSDN, Tao Tao’s CSDN

I’m the nuggetsThe nuggets of Zou Ah Tao Tao Tao