Android 菜单系统分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhenjie_chang/article/details/76942362

Android的菜单系统主要指的是ActionBar的Menu菜单。首先来看下Android菜单的使用方法:


@Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.test_menu_new,menu);
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        menu.removeItem(R.id.aaaa);
        return super.onPrepareOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        return super.onOptionsItemSelected(item);
    }

1.        在Activity刚创建的时候会执行一次onCreateOptionsMenu方法和onPrepareOptionsMenu方法。

2.        每次点击“更多”按钮的时候,都会执行一次onPrepareOptionsMenu方法。

3.        当选中某个菜单Item的时候执行onOptionsItemSelected方法

ActionBar菜单的创建加载过程。

ActionBar及ActionBar上的菜单都是在Activity启动的时候创建的。当一个新的Activity启动的时候,ActivityManagerService调用ActivityThread的performLaunchActivity方法,此方法中会调用Activity的attach方法,为Activity初始化一些变量。在Activity的attach方法中会为每一个Activity创建一个对应的PhoneWindow对象。然后在PhoneWindow中会创建ActionBar及ActionBar相关的菜单。

Activity在启动初始化过程中会调用onCreate方法,然后调用setContentView来设置Activity的显示内容。我们就从setContentView来简单分析下Activity的menu创建过程。

Activity.setContentView

public void setContentView(View view) {
        getWindow().setContentView(view);
        initWindowDecorActionBar();
    }

Activity的setContentView方法,调用了getWindow的setContentView方法。这个getWindow获取的就是Attach方法中创建的PhoneWindow对象。然后调用initWindowDecorActionBar方法,来初始化ActionBar操作的相关方法。

PhoneWindow.setContentView

public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }

public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            ……
        } else {
            mContentParent.addView(view, params);
        }
       ……
    }

PhoneWindow的setContentView方法调用了两个参数的setContentView方法。这个方法中只保留的比较重要的和我们分析相关的代码。

    首先调用installDecor方法来创建这个Activity对应的整个Window的界面布局。然后调用mContentParent.addView方法,将自己在setContentView方法中传过来的布局文件添加到mContentParent布局中。

PhoneWindow.installDecor

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
                PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
                if (!isDestroyed() && (st == null || st.menu == null) && !mIsStartingWindow) {
                    invalidatePanelMenu(FEATURE_ACTION_BAR);
                }
            } 
        }
    }

installDecor方法主要作用是生成和设置整个phoneWindow的窗口布局文件。首先调用generateDecor方法生成一个DecorView对象。然后调用generateLayout方法来添加生成窗口的布局文件,返回了一个mContentParent对象,contentParent对象就是setContentView的时候要把设置的布局文件 添加到这个View对象中,作为这个View的子View。然后从PhoneWindow的布局文件中找到id为decor_content_parent 的View对象,decorContentParent就是PhoneWindow布局文件的根布局文件。

    这个方法两个关键点:

1.  generateLayout方法生成窗口的布局文件

2.  invalidatePanelMenu创建菜单

AA

PhoneWindow.generateLayout

protected ViewGroup generateLayout(DecorView decor) {
        ……
        int layoutResource;
        int features = getLocalFeatures();
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            ……
            } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
                layoutResource = a.getResourceId(
                        R.styleable.Window_windowActionBarFullscreenDecorLayout,
                        R.layout.screen_action_bar);
            } else {
                layoutResource = R.layout.screen_title;
            }
            // System.out.println("Title!");
        } else 
            ……
        }

        mDecor.startChanging();
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        ……
        return contentParent;
    }

这部分关键代码如上所示,根据当前的feature来决定PhoneWindow加载那个布局文件。我们只关心有ActionBar的情况,所以PhoneWindow加载的布局文件是在theme主题的

windowActionBarFullscreenDecorLayout属性中配置的。

themes_material.xml

<style name="Theme.Material">
        <item name="windowActionBarFullscreenDecorLayout">@layout/screen_toolbar</item>
</style>

在Material主题中配置的screen_toolbar.xml布局文件。即在feature为FEATURE_ACTION_BAR的时候,加载screen_toolbar布局文件为整个PhoneWindow的布局。


<com.android.internal.widget.ActionBarOverlayLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/decor_content_parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:splitMotionEvents="false"
    android:theme="?attr/actionBarTheme">
    <FrameLayout android:id="@android:id/content"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent" />
    <com.android.internal.widget.ActionBarContainer
        android:id="@+id/action_bar_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        style="?attr/actionBarStyle"
        android:transitionName="android:action_bar"
        android:touchscreenBlocksFocus="true"
        android:gravity="top">
        <Toolbar
            android:id="@+id/action_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:navigationContentDescription="@string/action_bar_up_description"
            style="?attr/toolbarStyle" />
        <com.android.internal.widget.ActionBarContextView
            android:id="@+id/action_context_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="gone"
            style="?attr/actionModeStyle" />
    </com.android.internal.widget.ActionBarContainer>
</com.android.internal.widget.ActionBarOverlayLayout>

    Screen_toolbar布局把PhoneWindow的界面分为了两个部分,ActionBarContainer和Content。Content中就是显示Activity布局的地方。ActionBarContainer中包括ToolBar和ActionBarContextView。平时的情况下显示Toolbar,在ActionMode模式下,ActinBarContextView显示。

    ToolBar负责显示title,icon,subtitle,ActionBar menu等内容。

BB

   

布局文件生成并设置后,调用invalidatePanelMenu方法来生成并刷新Activity的菜单menu,invalidatePanelMenu最终调用到了doInvalidatePanelMenu方法中

PhoneWindow. doInvalidatePanelMenu

void doInvalidatePanelMenu(int featureId) {
        ……
        if ((featureId == FEATURE_ACTION_BAR || featureId == FEATURE_OPTIONS_PANEL)
                && mDecorContentParent != null) {
            st = getPanelState(Window.FEATURE_OPTIONS_PANEL, false);
            if (st != null) {
                st.isPrepared = false;
                preparePanel(st, null);
            }
        }
    }

在该方法中判断当前的featureId为FEATURE_ACTION_BAR或者FEATURE_OPTIONS_PANEL的时候,找到对应的PanelState对象,调用preparePanel方法对menu 菜单进行准备工作。

PhoneWindow.preparePanel

public final boolean preparePanel(PanelFeatureState st, KeyEvent event) {
        final Callback cb = getCallback();
        final boolean isActionBarMenu =
                (st.featureId == FEATURE_OPTIONS_PANEL || st.featureId == FEATURE_ACTION_BAR);

            if (st.menu == null || st.refreshMenuContent) {
                if (st.menu == null) {
                    if (!initializePanelMenu(st) || (st.menu == null)) {
                        return false;
                    }
                    mDecorContentParent.setMenu(st.menu, mActionMenuPresenterCallback);
                }
               ……
                if ((cb == null) || !cb.onCreatePanelMenu(st.featureId, st.menu)) {
                    // Ditch the menu created above
                    ……
                    return false;
                }
            ……
            if (!cb.onPreparePanel(st.featureId, st.createdPanelView, st.menu)) {
                ……
                return false;
            }

           ……
        return true;
    }

preparePanel方法主要处理菜单展示前的一些初始化操作,首先getCallback方法获取回调对象,此处的回调对象就是要显示的Activity的对象。

    在Activity启动的时候st.menu是null,所以调用initializePanelMenu方法来初始化st.menu对象。然后将初始化好的menu对象设置到mDecorContentParent中,mDecorContentParent是PhoneWIndow的根布局ActionBarOverlayLayout的对象。此处稍后再分析。

    初始化成功后,回调Activity的onCreatePanelMenu方法,来加载menu菜单。在回调onCreatePanelMenu方法的时候,会调用Activity的onCreateOptionsMenu方法来创建菜单,同时把创建的菜单对象menu作为参数传递了过去。

    然后回调Activity的onPreparePanel方法,在回调onPreparePanel的时候会调用onPrepareOptionsMenu方法

    到此为止,菜单的 onCreateOptionsMenu 和onPrepareOptionsMenu方法调用的逻辑就分析完成了,下边接着分析Menu的加载过程。

Android菜单内容的加载过程

public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.test_menu_new,menu);
        return true;
    }

在重写Activity的onCreateOptionsMenu方法中,参数Menu就是PhoneWindow中创建的menu对象,getMenuInflater方法获取的一个MenuInflater的对象,然后调用MenuInflater的inflate方法将test_menu_new.xml文件中的菜单解析并保存到对象menu中。

    MenuInflater类主要负责menu.xml菜单的解析,并将解析的内容保存到Menu对象中。

private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)
            throws XmlPullParserException, IOException {
        MenuState menuState = new MenuState(menu);
        ……
        while (!reachedEndOfMenu) {
            switch (eventType) {
                case XmlPullParser.START_TAG:
                    ……
                    if (tagName.equals(XML_GROUP)) {
                        menuState.readGroup(attrs);
                    } else if (tagName.equals(XML_ITEM)) {
                        menuState.readItem(attrs);
                    } else if (tagName.equals(XML_MENU)) {
                       ……
                    } else {
                        lookingForEndOfUnknownTag = true;
                        unknownTagName = tagName;
                    }
                    break;
                    
                case XmlPullParser.END_TAG:
                    if (tagName.equals(XML_ITEM)) {
                        if (!menuState.hasAddedItem()) {
                            if (menuState.itemActionProvider != null &&
                                    menuState.itemActionProvider.hasSubMenu()) {
                                registerMenu(menuState.addSubMenuItem(), attrs);
                            } else {
                                registerMenu(menuState.addItem(), attrs);
                            }
                        }
                    } 
                    ……
            }
            
            eventType = parser.next();
        }
    }

在解析菜单文件的时候,根据menu创建了一个MenuState对象,调用menuState的readItem来读取xml中的属性信息,并保存到menuState的变量中。在读取完某个item的所有属性后,调用menuState.addItem方法将读取的item信息保存到系统的Menu对象中

public MenuItem addItem() {
            itemAdded = true;
            MenuItem item = menu.add(groupId, itemId, itemCategoryOrder, itemTitle);
            setItem(item);
            return item;
        }

调用menu.add方法将解析的Menu item信息保存到menu中。这样就把menu.xml文件中的所有的菜单item都保存到了菜单menu中。

    菜单主要包括Menu,SubMenu以及MenuItem

    Menu是整个菜单的容器,Menu中其中包括SubMenu.Menu及SubMenu中有一些MenuItem类型的ArrayList列表。解析的menu.xml文件的item信息就保存到了Menu类的变量ArrayList<MenuItemImpl> mItems中。

    几个menu相关类的关系如图。

CCCC

    Menu.xml文件解析完成之后,就把menu.xml文件中的menu的信息保存到了传递过来的参数menu中。

Menu接口

public interface Menu {
    public MenuItem add(CharSequence title);
    public MenuItem add(@StringRes int titleRes);
    SubMenu addSubMenu(final CharSequence title);
    SubMenu addSubMenu(@StringRes final int titleRes);
    public void removeItem(int id);
    public MenuItem getItem(int index);
    public boolean performIdentifierAction(int id, int flags);
}

SubMenu 接口

public interface SubMenu extends Menu {
    public SubMenu setHeaderTitle(@StringRes int titleRes);
    public SubMenu setHeaderTitle(CharSequence title);
    public SubMenu setHeaderIcon(@DrawableRes int iconRes);
    public SubMenu setHeaderIcon(Drawable icon);
    public SubMenu setHeaderView(View view);
    public MenuItem getItem();
}

MenuItem接口

public interface MenuItem {
    public int getItemId();
    public int getGroupId();
    public MenuItem setTitle(CharSequence title);
    public CharSequence getTitle();
    public MenuItem setIcon(Drawable icon);
    public Drawable getIcon();
    public MenuItem setVisible(boolean visible);
    public boolean isVisible();
    public MenuItem setEnabled(boolean enabled);
    public boolean isEnabled();
}

接着分析mDecorContentParent.setMenu方法。mDecorContentParent是ActionBarOverlayLayout的对象,在ActionBarOverlayLayout的setMenu方法中,ActionBarOverlayLayout获取到他的子类id 为actionBar的ToolBar,调用ToolBar的包装类ToolbarWidgetWraper的setMenu. ToolbarWidgetWraper又调用ToolBar的setMenu方法。

ToolbarWidgetWraper.setMenu

public void setMenu(Menu menu, MenuPresenter.Callback cb) {
        if (mActionMenuPresenter == null) {
            mActionMenuPresenter = new ActionMenuPresenter(mToolbar.getContext());
            mActionMenuPresenter.setId(com.android.internal.R.id.action_menu_presenter);
        }
        mActionMenuPresenter.setCallback(cb);
        mToolbar.setMenu((MenuBuilder) menu, mActionMenuPresenter);
    }

首先创建了一个ActionMenuPresenter的对象,然后以ActionMenuPresenter的对象和menu作为参数,调用Toolbar的setMenu方法。

private void ensureMenuView() {
        if (mMenuView == null) {
            mMenuView = new ActionMenuView(getContext());
            mMenuView.setPopupTheme(mPopupTheme);
            mMenuView.setOnMenuItemClickListener(mMenuViewItemClickListener);
            mMenuView.setMenuCallbacks(mActionMenuPresenterCallback, mMenuBuilderCallback);
            final LayoutParams lp = generateDefaultLayoutParams();
            lp.gravity = Gravity.END | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK);
            mMenuView.setLayoutParams(lp);
            addSystemView(mMenuView, false);
        }
    }
    
 public void setMenu(MenuBuilder menu, ActionMenuPresenter outerPresenter) {
        
        ensureMenuView();
        final MenuBuilder oldMenu = mMenuView.peekMenu();
        
        if (oldMenu != null) {
            oldMenu.removeMenuPresenter(mOuterActionMenuPresenter);
            oldMenu.removeMenuPresenter(mExpandedMenuPresenter);
        }

       
        if (menu != null) {
            menu.addMenuPresenter(outerPresenter, mPopupContext);
            menu.addMenuPresenter(mExpandedMenuPresenter, mPopupContext);
        }
        mMenuView.setPresenter(outerPresenter);
    }

Toolbar的setMenu方法首先调用ensureMenuView方法创建一个ActionMenuView对象,然后调用addSystemView方法将它添加到Toolbar中,ActionMenuView是继承自LinearLayout类,负责Toolbar上Menu的显示。

dddd

ActionMenuPresenter是ActionMenuView对应的一个管理类,ActionMenuView继承自LinearLayout,负责Menu的显示,而ActionMenuPresenter负责Menu的显示逻辑,负责将加载的Menu信息,按照对应的逻辑添加到ActionMenuView中。

将创建的ActionMenuPresenter对象调用menu.addMenuPresenter方法添加到Menu的变量presenters列表中。

eeee

ActionMenuView菜单显示逻辑的简单分析:ActionMenuView的显示逻辑比较复杂,主要由ActionMenuPresenter类控制,我们主要从以下几个步骤来分析:

1.  ActionMenuPresenter.initForMenu

2.  ActionMenuPresenter.getMenuView

3.  ActionMenuPresenter.updateMenuView

ActionMenuPresenter.initForMenu

public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) {
        super.initForMenu(context, menu);

        final Resources res = context.getResources();

        if (mReserveOverflow) {
            if (mOverflowButton == null) {
                mOverflowButton = new OverflowMenuButton(mSystemContext);
                if (mPendingOverflowIconSet) {
                    mOverflowButton.setImageDrawable(mPendingOverflowIcon);
                    mPendingOverflowIcon = null;
                    mPendingOverflowIconSet = false;
                }
            }
        } else {
            mOverflowButton = null;
        }
    }

首先调用BaseMenuPresenter的initForMenu方法,保存传入的参数MenuBuilder的值。然后创建了一个OverflowMenuButton,当ActionMenuView需要显示更多的时候,ActionMenuView应该添加OverflowMenuButton.

 public MenuView getMenuView(ViewGroup root) {
        MenuView oldMenuView = mMenuView;
        MenuView result = super.getMenuView(root);
        return result;
    }

ActionMenuPresenter的getMenuView方法调用的是BaseMenuPresenter的getMenuView方法。

BaseMenuPresenter.getMenuView

public MenuView getMenuView(ViewGroup root) {
        if (mMenuView == null) {
            mMenuView = (MenuView) mSystemInflater.inflate(mMenuLayoutRes, root, false);
            mMenuView.initialize(mMenu);
        }
        return mMenuView;
    }

在BaseMenuPresenter.getMenuView方法中使用inflate方法加载mMenuLayoutRes文件来创建了一个MenuView的对象,并调用MenuView的initialize方法初始化了MenuView对象。

ActionMenuPresenter.updateMenuView

在分析ActionMenuPresenter的updateMenuView方法时,我们应该首先分析下ActionMenuPresenter父类BaseMenuPresenter的updateMenuView方法。

BaseMenuPresenter.updateMenuView

public void updateMenuView(boolean cleared) {
        final ViewGroup parent = (ViewGroup) mMenuView;
        if (parent == null) return;

        int childIndex = 0;
        if (mMenu != null) {
            mMenu.flagActionItems();
            ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems();
            final int itemCount = visibleItems.size();
            for (int i = 0; i < itemCount; i++) {
                MenuItemImpl item = visibleItems.get(i);
                ……
                    addItemView(itemView, childIndex);
                    childIndex++;
                }
            }
        }
    }

在这个方法中首先调用mMenu.flagActionItems()方法来遍历设置每个菜单条目是ActionItems还是NoActionItems.根据不同的类型分别添加到不同的列表中。

ActionItems显示在ActionBar上,NoActionItems显示在更多的弹出菜单中。

public void flagActionItems() {
        final ArrayList<MenuItemImpl> visibleItems = getVisibleItems();

        //调用对应的MenuPresenter来确定MenuItem的分类,ActionItem还是NoActionItem
        boolean flagged = false;
        for (WeakReference<MenuPresenter> ref : mPresenters) {
            final MenuPresenter presenter = ref.get();
            if (presenter == null) {
                mPresenters.remove(ref);
            } else {
                flagged |= presenter.flagActionItems();
            }
        }

        if (flagged) {
            mActionItems.clear();
            mNonActionItems.clear();
            final int itemsSize = visibleItems.size();
            for (int i = 0; i < itemsSize; i++) {
                MenuItemImpl item = visibleItems.get(i);
                if (item.isActionButton()) {
                    //ActionButton 添加到mActionItems列表
                    mActionItems.add(item);
                } else {
                    //noActionItem添加到mNonActionItems列表
                    mNonActionItems.add(item);
                }
            }
        } else {
            mActionItems.clear();
            mNonActionItems.clear();
            mNonActionItems.addAll(getVisibleItems());
        }
    }

ActionMenuPresenter.updateMenuView方法主要处理逻辑就是根据NoActionItems数量来决定是否显示OverflowMenuButton,即更多按钮。当noActionItems的数量大于1,就表示有Item需要显示在更多的弹出菜单中,就需要显示更多按钮了。调用ActionMenuView的addView方法,添加“更多”按钮到ActionMenuView的末尾。

    接着分析点击更多是弹出菜单显示逻辑。点击更多菜单调用showOverflowMenu方法。弹出菜单是由一个ListPopWindow来实现的.

ActionMenuPresenter.showOverflowMenu

OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true);
mPostedOpenRunnable = new OpenOverflowRunnable(popup);
((View) mMenuView).post(mPostedOpenRunnable);

showOverflowmenu中创建了一个OverflowPopup对象和一个OpenOverflowRunable对象。OverflowPopup继承自MenuPopupHelper,然后调用OpenOverflowRunnable中的方法。

private class OpenOverflowRunnable implements Runnable {
        private OverflowPopup mPopup;

        public OpenOverflowRunnable(OverflowPopup popup) {
            mPopup = popup;
        }

        public void run() {
            /// M: Add NULL pointer check
            if (mMenu != null) {
                mMenu.changeMenuMode();
            }
            final View menuView = (View) mMenuView;
            mPopup.tryShow();
        }
    }

Runable方法中首先调用了Menu.changeMenuMode(), MenuBuilder的changMenuMode方法回调PhoneWindow的onMenuModeChanged方法。最终回调Activity的onPrepareOptionsMenu方法。这边就是为什么每次点击更多按钮的时候会回调Activity的onPrepareOptionsMenu方法。

然后调用MenuPopupHelper.tryShow方法。
public boolean tryShow() {
        if (isShowing()) {
            return true;
        }

        mPopup = new ListPopupWindow(mContext, null, mPopupStyleAttr, mPopupStyleRes);

        mPopup.setOnItemClickListener(this);
        mPopup.setAdapter(mAdapter);
        ……

        mPopup.show();
        return true;
    }

tryShow方法中创建了一个ListPopupWindow对象,设置他的adapter,ListPopupWindow的Adatper是根据menu.getNoActionItems来创建的,NoActionItems列表中的菜单会显示在ListPopupWindow中然后调用ListPopupWindow对象的show方法,这样ActionBar的menu菜单点击更多按钮的弹出菜单就可以显示出来了。

    ActionBar菜单实现的总结:

1.  在创建Activity的时候,PhoneWindow加载窗口布局包括对应的ActionBar,创建Menu对象,并把创建的Menu设置到Activity的ActionBar中

2.  调用invalidateOptionsPanel方法,初始化菜单,回调Activity的onCreateOptionsMenu,解析menu.xml文件,保存到Menu中。(包括Menu,SubMenu及MenuItem的实现)。

3.  ActionMenuView是ActionBar中负责显示菜单的View,继承自LinearLayout,具体的显示逻辑由ActionMenuPresenter类来实现。

ActionMenuPresenter的updateMenuView负责菜单的显示逻辑,决定哪些Item显示在ActionBar上,哪些Item显示在弹出菜单中。显示在ActionBar的菜单Item添加到ActionMenuView中,显示在弹出菜单的Item添加到ListPopupWindow中。



猜你喜欢

转载自blog.csdn.net/zhenjie_chang/article/details/76942362