• Preference深度解析


    在阅读Android系统应用Settings源码的时候,我们会发现它的布局文件有一大批Preference控件,这个是什么东西呢?其实Preference是android原生为了持久化数据的一种"控件",注意它并不是真正意义上的View。

    一、初识Preference

    Preference家族代码被定义在frameworks/base/core/java/android/preference,改目录下的代码用来实现偏好设置相关的界面。即Preference是android原生为了持久化数据的一种"控件"。要使用Preference就必须建立在PreferenceActivity或PreferenceFragment的载体上面。具体用法可以参考《Android之PreferenceFragment详解》

    • Preference家族

    Preference被定义在frameworks/base/core下面,其中Preference作为视图控件在整个家族中的地位类似于View,PreferenceGroup就类似于ViewGroup,XXXPreference则继承于Preference并扩展了不同功能样式。除此之外Preference并不能直接使用在Activity和Fragment上面,因此多了PreferenceActivity和PreferenceFragment他们分别继承Activity和Fragment并在此基础上实现了数据持久化、控件解析等功能。除此之外还有两个比较重要的类: PreferenceScreenPreferenceGroupAdapter

    • Preference是"控件"?

    Preference并不是控件,它没有继承View,但是为什么我们的布局文件中可以使用他呢?其实只有继承了PreferenceFragment或者PreferenceActivity的才能使用上面以PreferenceScreen标签开头的xml文件,因为他们内部作了一系列解析,同时Preference有方法返回一个View对象,如下代码:

    1. //android/frameworks/base/core/java/android/preference/Preference.java
    2. @Deprecated
    3. public class Preference implements Comparable {
    4. private boolean mShouldDisableView = true;
    5. @UnsupportedAppUsage
    6. private int mLayoutResId = com.android.internal.R.layout.preference;
    7. @UnsupportedAppUsage
    8. private int mWidgetLayoutResId;
    9. public void setLayoutResource(@LayoutRes int layoutResId) {
    10. if (layoutResId != mLayoutResId) mRecycleEnabled = false;
    11. mLayoutResId = layoutResId;
    12. }
    13. @LayoutRes
    14. public int getLayoutResource() {
    15. return mLayoutResId;
    16. }
    17. //返回一个视图控件View
    18. public View getView(View convertView, ViewGroup parent) {
    19. //如果第一次就创建视图View
    20. if (convertView == null) convertView = onCreateView(parent);
    21. //给视图View填充数据和更新ui
    22. onBindView(convertView);
    23. return convertView;
    24. }
    25. //创建视图控件View,实际上还是inflate对应的资源ID文件
    26. @CallSuper
    27. protected View onCreateView(ViewGroup parent) {
    28. final LayoutInflater layoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    29. final View layout = layoutInflater.inflate(mLayoutResId, parent, false);
    30. final ViewGroup widgetFrame = (ViewGroup) layout.findViewById(com.android.internal.R.id.widget_frame);
    31. if (widgetFrame != null) {
    32. if (mWidgetLayoutResId != 0) layoutInflater.inflate(mWidgetLayoutResId, widgetFrame);
    33. else widgetFrame.setVisibility(View.GONE);
    34. }
    35. return layout;
    36. }
    37. //主要是给上面创建的视图View设置数据
    38. @CallSuper
    39. protected void onBindView(View view) {
    40. final TextView titleView = (TextView) view.findViewById(com.android.internal.R.id.title);
    41. if (titleView != null) {
    42. final CharSequence title = getTitle();
    43. if (!TextUtils.isEmpty(title)) {
    44. titleView.setText(title);
    45. titleView.setVisibility(View.VISIBLE);
    46. if (mHasSingleLineTitleAttr) titleView.setSingleLine(mSingleLineTitle);
    47. } else {
    48. titleView.setVisibility(View.GONE);
    49. }
    50. }
    51. final TextView summaryView = (TextView) view.findViewById( com.android.internal.R.id.summary);
    52. if (summaryView != null) {
    53. final CharSequence summary = getSummary();
    54. if (!TextUtils.isEmpty(summary)) {
    55. summaryView.setText(summary);
    56. summaryView.setVisibility(View.VISIBLE);
    57. } else {
    58. summaryView.setVisibility(View.GONE);
    59. }
    60. }
    61. final ImageView imageView = (ImageView) view.findViewById(com.android.internal.R.id.icon);
    62. if (imageView != null) {
    63. if (mIconResId != 0 || mIcon != null) {
    64. if (mIcon == null) mIcon = getContext().getDrawable(mIconResId);
    65. if (mIcon != null) imageView.setImageDrawable(mIcon);
    66. }
    67. if (mIcon != null) imageView.setVisibility(View.VISIBLE);
    68. else imageView.setVisibility(mIconSpaceReserved ? View.INVISIBLE : View.GONE);
    69. }
    70. final View imageFrame = view.findViewById(com.android.internal.R.id.icon_frame);
    71. if (imageFrame != null) {
    72. if (mIcon != null) imageFrame.setVisibility(View.VISIBLE);
    73. else imageFrame.setVisibility(mIconSpaceReserved ? View.INVISIBLE : View.GONE);
    74. }
    75. if (mShouldDisableView) setEnabledStateOnViews(view, isEnabled());
    76. }
    77. //界面更改条目栏
    78. public void setTitle(CharSequence title) {
    79. if (title == null && mTitle != null || title != null && !title.equals(mTitle)) {
    80. mTitleRes = 0;
    81. mTitle = title;
    82. notifyChanged();
    83. }
    84. }
    85. //界面更改图标
    86. public void setIcon(Drawable icon) {
    87. if ((icon == null && mIcon != null) || (icon != null && mIcon != icon)) {
    88. mIcon = icon;
    89. notifyChanged();
    90. }
    91. }
    92. //界面有改变回调监听器
    93. protected void notifyChanged() {
    94. if (mListener != null) mListener.onPreferenceChange(this);
    95. }
    96. }
    • Preference如何持久化?

    Settings为什么大量使用了Preference,而没有使用到我们常见的View和TextView呢,因为这里逻辑功能上主要是为了给系统进行一些设置,所有就涉及到了设置参数持久化(即断电后还继续生效),如下代码它内部已经通过PreferenceManager来进行对数据的持久化,实际上还是使用了四大存储方式之一

    1. //android/frameworks/base/core/java/android/preference/Preference.java
    2. @Deprecated
    3. public class Preference implements Comparable {
    4. @Nullable
    5. private PreferenceManager mPreferenceManager;
    6. public SharedPreferences getSharedPreferences() {
    7. if (mPreferenceManager == null || getPreferenceDataStore() != null) return null;
    8. return mPreferenceManager.getSharedPreferences();
    9. }
    10. public SharedPreferences.Editor getEditor() {
    11. if (mPreferenceManager == null || getPreferenceDataStore() != null) return null;
    12. return mPreferenceManager.getEditor();
    13. }
    14. public boolean shouldCommit() {
    15. if (mPreferenceManager == null) return false;
    16. return mPreferenceManager.shouldCommit();
    17. }
    18. public PreferenceManager getPreferenceManager() {
    19. return mPreferenceManager;
    20. }
    21. }
    • Preference唯一标识
    • Preference跳转到Fragment

    二、深度剖析

    第一章初步介绍了Preference的基本用法和一些特点,想使用Preference必须建立在PreferenceActivity或者PreferenceFragment的基础之上,除此之外布局文件最外层必须使用一个PreferenceScreen嵌套所有的Preference。

    1、剖析PreferenceActivity

    1.1、布局文件加载

    PreferenceActivity的布局跟主流布局类似,即通常由三部分组成:标题栏、内容、最底层的bar。这里我们关注标题栏和内容,因为PreferenceActivity在设计的时候就考虑到了公用同一个activity进行页面跳转,即替换内容区域的fragment。

    1. //frameworks/base/core/java/android/preference/PreferenceActivity.java
    2. /*继承ListActivity,即其布局文件中必须有一个ListView控件且id必须为list*/
    3. @Deprecated public abstract class PreferenceActivity extends ListActivity implements
    4. /*Preference树结构下所有视图控件点击回调事件*/
    5. PreferenceManager.OnPreferenceTreeClickListener,
    6. /*Preference被点击后如果有设置fragment就直接替换fragment回调*/
    7. PreferenceFragment.OnPreferenceStartFragmentCallback {
    8. //通过android:fragment属性进行跳转不同fragment页面,可能共用同一个PreferenceActivity,但是标题栏不一样,mHeaders 存储了所有的标题
    9. private final ArrayList
      mHeaders = new ArrayList
      ();
    10. private FrameLayout mListFooter;
    11. private ViewGroup mPrefsContainer;
    12. private CharSequence mActivityTitle;
    13. private ViewGroup mHeadersContainer;
    14. //当前对应的标题,Header表示一个标题
    15. private Header mCurHeader;
    16. //管理器
    17. private PreferenceManager mPreferenceManager;
    18. //数据持久化
    19. private Bundle mSavedInstanceState;
    20. @Override
    21. protected void onCreate(@Nullable Bundle savedInstanceState) {
    22. super.onCreate(savedInstanceState);
    23. //布局文件加载的是preference_list_content.xml
    24. final int layoutResId = sa.getResourceId(
    25. com.android.internal.R.styleable.PreferenceActivity_layout,
    26. com.android.internal.R.layout.preference_list_content);
    27. setContentView(layoutResId);
    28. mListFooter = (FrameLayout)findViewById(com.android.internal.R.id.list_footer);
    29. mPrefsContainer = (ViewGroup) findViewById(com.android.internal.R.id.prefs_frame);
    30. mHeadersContainer = (ViewGroup) findViewById(com.android.internal.R.id.headers);
    31. //......
    32. }
    33. }
    1. //frameworks/base/core/res/res/layout/preference_list_content.xml
    2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3. android:orientation="vertical"
    4. android:layout_height="match_parent"
    5. android:layout_width="match_parent">
    6. <LinearLayout
    7. android:id="@+id/prefs_container"
    8. android:orientation="horizontal"
    9. android:layout_width="match_parent"
    10. android:layout_height="0px"
    11. android:layout_weight="1">
    12. <LinearLayout
    13. style="?attr/preferenceHeaderPanelStyle"
    14. android:id="@+id/headers"
    15. android:orientation="vertical"
    16. android:layout_width="0px"
    17. android:layout_height="match_parent"
    18. android:layout_weight="@integer/preferences_left_pane_weight">
    19. <ListView android:id="@android:id/list"
    20. style="?attr/preferenceListStyle"
    21. android:layout_width="match_parent"
    22. android:layout_height="0px"
    23. android:layout_weight="1"
    24. android:clipToPadding="false"
    25. android:drawSelectorOnTop="false"
    26. android:cacheColorHint="@android:color/transparent"
    27. android:listPreferredItemHeight="48dp"
    28. android:scrollbarAlwaysDrawVerticalTrack="true" />
    29. <FrameLayout android:id="@+id/list_footer"
    30. android:layout_width="match_parent"
    31. android:layout_height="wrap_content"
    32. android:layout_weight="0" />
    33. LinearLayout>
    34. <LinearLayout
    35. android:id="@+id/prefs_frame"
    36. style="?attr/preferencePanelStyle"
    37. android:layout_width="0px"
    38. android:layout_height="match_parent"
    39. android:layout_weight="@integer/preferences_right_pane_weight"
    40. android:orientation="vertical">
    41. <include layout="@layout/breadcrumbs_in_fragment" />
    42. <android.preference.PreferenceFrameLayout android:id="@+id/prefs"
    43. android:layout_width="match_parent"
    44. android:layout_height="0dip"
    45. android:layout_weight="1"
    46. />
    47. LinearLayout>
    48. LinearLayout>
    49. LinearLayout>

    1.2、Header对应fragment的标题

    1. //frameworks/base/core/java/android/preference/PreferenceActivity.java
    2. @Deprecated public abstract class PreferenceActivity extends ListActivity{
    3. //Header对应当前界面,即存储了当前界面的基本信息:
    4. // titleRes和title为标题栏
    5. // summaryRes和summary为概述
    6. @Deprecated public static final class Header implements Parcelable {
    7. @StringRes
    8. public int titleRes;
    9. public CharSequence title;
    10. @StringRes
    11. public int summaryRes;
    12. public CharSequence summary;
    13. //对应的ICON图标
    14. public int iconRes;
    15. //存储了对应的fragment的类名,点击Preference会自动跳转到android:fragment属性的值对应的类(通过反射进行实例化)
    16. public String fragment;
    17. //需要传递到fragment的参数
    18. public Bundle fragmentArguments;
    19. public Intent intent;
    20. public Bundle extras;
    21. //序列化上面的内容,包括fragment的类名
    22. @Override public void writeToParcel(Parcel dest, int flags) {
    23. dest.writeLong(id);
    24. dest.writeInt(titleRes);
    25. TextUtils.writeToParcel(title, dest, flags);
    26. dest.writeInt(summaryRes);
    27. TextUtils.writeToParcel(summary, dest, flags);
    28. dest.writeInt(breadCrumbTitleRes);
    29. TextUtils.writeToParcel(breadCrumbTitle, dest, flags);
    30. dest.writeInt(breadCrumbShortTitleRes);
    31. TextUtils.writeToParcel(breadCrumbShortTitle, dest, flags);
    32. dest.writeInt(iconRes);
    33. dest.writeString(fragment);
    34. dest.writeBundle(fragmentArguments);
    35. if (intent != null) {
    36. dest.writeInt(1);
    37. intent.writeToParcel(dest, flags);
    38. } else dest.writeInt(0);
    39. dest.writeBundle(extras);
    40. }
    41. //反序列化上面的内容,包括fragment的类名
    42. public void readFromParcel(Parcel in) {
    43. id = in.readLong();
    44. titleRes = in.readInt();
    45. title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
    46. summaryRes = in.readInt();
    47. summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
    48. breadCrumbTitleRes = in.readInt();
    49. breadCrumbTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
    50. breadCrumbShortTitleRes = in.readInt();
    51. breadCrumbShortTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
    52. iconRes = in.readInt();
    53. fragment = in.readString();
    54. fragmentArguments = in.readBundle();
    55. if (in.readInt() != 0) intent = Intent.CREATOR.createFromParcel(in);
    56. extras = in.readBundle();
    57. }
    58. Header(Parcel in) {
    59. readFromParcel(in);
    60. }
    61. public static final @android.annotation.NonNull Creator
      CREATOR = new Creator
      () {
    62. public Header createFromParcel(Parcel source) {
    63. return new Header(source);
    64. }
    65. public Header[] newArray(int size) {
    66. return new Header[size];
    67. }
    68. };
    69. }
    70. private static class HeaderAdapter extends ArrayAdapter
      {
    71. private static class HeaderViewHolder {
    72. ImageView icon;
    73. TextView title;
    74. TextView summary;
    75. }
    76. private LayoutInflater mInflater;
    77. private int mLayoutResId;
    78. private boolean mRemoveIconIfEmpty;
    79. public HeaderAdapter(Context context, List
      objects, int layoutResId, boolean removeIconBehavior) {
    80. super(context, 0, objects);
    81. mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    82. mLayoutResId = layoutResId;
    83. mRemoveIconIfEmpty = removeIconBehavior;
    84. }
    85. @Override
    86. public View getView(int position, View convertView, ViewGroup parent) {
    87. HeaderViewHolder holder;
    88. View view;
    89. if (convertView == null) {
    90. view = mInflater.inflate(mLayoutResId, parent, false);
    91. holder = new HeaderViewHolder();
    92. holder.icon = (ImageView) view.findViewById(com.android.internal.R.id.icon);
    93. holder.title = (TextView) view.findViewById(com.android.internal.R.id.title);
    94. holder.summary = (TextView) view.findViewById(com.android.internal.R.id.summary);
    95. view.setTag(holder);
    96. } else {
    97. view = convertView;
    98. holder = (HeaderViewHolder) view.getTag();
    99. }
    100. }
    101. }
    102. }

    1.3、切换fragment

    1. //frameworks/base/core/java/android/preference/PreferenceActivity.java
    2. @Deprecated public abstract class PreferenceActivity extends ListActivity{
    3. //该方法很多地方被调用,例如创建或者点击需要切换fragment或者当前header的时候
    4. public void switchToHeader(Header header) {
    5. //判断是否当前header,是不切换
    6. if (mCurHeader == header) {
    7. getFragmentManager().popBackStack(BACK_STACK_PREFS, FragmentManager.POP_BACK_STACK_INCLUSIVE);
    8. } else {
    9. if (header.fragment == null) throw new IllegalStateException("can't switch to header that has no fragment");
    10. //切换对应的fragment
    11. switchToHeaderInner(header.fragment, header.fragmentArguments);
    12. //切换header布局里面的字符串和图标等信息
    13. setSelectedHeader(header);
    14. }
    15. }
    16. //还是使用了FragmentTransaction 方式进行切换
    17. private void switchToHeaderInner(String fragmentName, Bundle args) {
    18. getFragmentManager().popBackStack(BACK_STACK_PREFS, FragmentManager.POP_BACK_STACK_INCLUSIVE);
    19. if (!isValidFragment(fragmentName)) throw new IllegalArgumentException("Invalid fragment for this activity: " + fragmentName);
    20. Fragment f = Fragment.instantiate(this, fragmentName, args);
    21. FragmentTransaction transaction = getFragmentManager().beginTransaction();
    22. transaction.setTransition(mSinglePane
    23. ? FragmentTransaction.TRANSIT_NONE
    24. : FragmentTransaction.TRANSIT_FRAGMENT_FADE);
    25. transaction.replace(com.android.internal.R.id.prefs, f);
    26. transaction.commitAllowingStateLoss();
    27. if (mSinglePane && mPrefsContainer.getVisibility() == View.GONE) {
    28. mPrefsContainer.setVisibility(View.VISIBLE);
    29. mHeadersContainer.setVisibility(View.GONE);
    30. }
    31. }
    32. //切换header的内容,通过触发点击事件
    33. void setSelectedHeader(Header header) {
    34. mCurHeader = header;
    35. int index = mHeaders.indexOf(header);
    36. if (index >= 0) getListView().setItemChecked(index, true);
    37. else getListView().clearChoices();
    38. showBreadCrumbs(header);
    39. }
    40. }

    2、剖析PreferenceFragment

    2.1、布局文件加载

    PreferenceFragment的布局跟PreferenceActivity类似,如下代码:

    1. //frameworks/base/core/java/android/preference/PreferenceFragment.java
    2. @Deprecated public abstract class PreferenceFragment extends Fragment implements
    3. /*Preference树结构下所有视图控件点击回调事件*/
    4. PreferenceManager.OnPreferenceTreeClickListener {
    5. @UnsupportedAppUsage
    6. private PreferenceManager mPreferenceManager;
    7. private ListView mList;
    8. //PreferenceFragment的布局文件,与PreferenceActivity使用了同样的布局
    9. private int mLayoutResId = com.android.internal.R.layout.preference_list_fragment;
    10. private static final int MSG_BIND_PREFERENCES = 1;
    11. private Handler mHandler = new Handler() {
    12. @Override public void handleMessage(Message msg) {
    13. switch (msg.what) {
    14. case MSG_BIND_PREFERENCES:
    15. bindPreferences();
    16. break;
    17. }
    18. }
    19. };
    20. @Override
    21. public void onCreate(@Nullable Bundle savedInstanceState) {
    22. super.onCreate(savedInstanceState);
    23. mPreferenceManager = new PreferenceManager(getActivity(), FIRST_REQUEST_CODE);
    24. mPreferenceManager.setFragment(this);
    25. }
    26. @Override
    27. public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    28. //加载preference_list_fragment.xml,如果有acitivity沿用
    29. mLayoutResId = a.getResourceId(com.android.internal.R.styleable.PreferenceFragment_layout, mLayoutResId);
    30. return inflater.inflate(mLayoutResId, container, false);
    31. }
    32. //从布局文件中找到list
    33. @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    34. super.onViewCreated(view, savedInstanceState);
    35. ListView lv = (ListView) view.findViewById(android.R.id.list);
    36. if (lv != null && a.hasValueOrEmpty(com.android.internal.R.styleable.PreferenceFragment_divider)) {
    37. lv.setDivider( a.getDrawable(com.android.internal.R.styleable.PreferenceFragment_divider));
    38. }
    39. }
    40. //这里很重要
    41. @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    42. super.onActivityCreated(savedInstanceState);
    43. //bindPreferences方法是用来绑定加载所有的Preference类的
    44. if (mHavePrefs) bindPreferences();
    45. mInitDone = true;
    46. if (savedInstanceState != null) {
    47. Bundle container = savedInstanceState.getBundle(PREFERENCES_TAG);
    48. if (container != null) {
    49. //如果有缓存PreferenceScreen,直接获取出来
    50. final PreferenceScreen preferenceScreen = getPreferenceScreen();
    51. if (preferenceScreen != null) preferenceScreen.restoreHierarchyState(container);
    52. }
    53. }
    54. }
    55. //设置Preference点击事件
    56. @Override public void onStart() {
    57. super.onStart();
    58. mPreferenceManager.setOnPreferenceTreeClickListener(this);
    59. }
    60. @Override public void onStop() {
    61. super.onStop();
    62. mPreferenceManager.dispatchActivityStop();
    63. mPreferenceManager.setOnPreferenceTreeClickListener(null);
    64. }
    65. }

    2.2、什么时候回调onBindPreferences方法?

    1. public abstract class PreferenceFragment{
    2. private static final int MSG_BIND_PREFERENCES = 1;
    3. private Handler mHandler = new Handler() {
    4. @Override public void handleMessage(Message msg) {
    5. switch (msg.what) {
    6. case MSG_BIND_PREFERENCES:
    7. bindPreferences();
    8. break;
    9. }
    10. }
    11. };
    12. private void bindPreferences() {
    13. //获取当前对应的PreferenceScreen
    14. final PreferenceScreen preferenceScreen = getPreferenceScreen();
    15. if (preferenceScreen != null) {
    16. View root = getView();
    17. if (root != null) {
    18. View titleView = root.findViewById(android.R.id.title);
    19. if (titleView instanceof TextView) {
    20. CharSequence title = preferenceScreen.getTitle();
    21. if (TextUtils.isEmpty(title)) {
    22. titleView.setVisibility(View.GONE);
    23. } else {
    24. ((TextView) titleView).setText(title);
    25. titleView.setVisibility(View.VISIBLE);
    26. }
    27. }
    28. }
    29. preferenceScreen.bind(getListView());
    30. }
    31. //回调子类(继承于PreferenceFragment中的onBindPreferences方法,通常在子类该方法钟回调所有需要显示的Preference控件)
    32. onBindPreferences();
    33. }
    34. //通过mPreferenceManager获取当前的PreferenceScreen
    35. public PreferenceScreen getPreferenceScreen() {
    36. return mPreferenceManager.getPreferenceScreen();
    37. }
    38. //通过mPreferenceManager设置当前的PreferenceScreen
    39. public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
    40. if (mPreferenceManager.setPreferences(preferenceScreen) && preferenceScreen != null) {
    41. onUnbindPreferences();
    42. mHavePrefs = true;
    43. if (mInitDone) postBindPreferences();
    44. }
    45. }
    46. //这个PreferenceScreen怎么来的呢,最终还是用户调用该接口设置进去的
    47. public void addPreferencesFromResource(@XmlRes int preferencesResId) {
    48. requirePreferenceManager();
    49. setPreferenceScreen(mPreferenceManager.inflateFromResource(getActivity(), preferencesResId, getPreferenceScreen()));
    50. }
    51. }

  • 相关阅读:
    nordic 52832中添加RTT打印
    --initialize specified but the data directory has files in it. Aborting. 问题解决
    2023蓝帽杯半决赛misc题目复现
    Java重写与重载
    基于深度强化学习的智能汽车决策模型
    2024022502-数据库绪论
    搭建docker镜像仓库
    java计算机毕业设计-学生宿舍故障报修管理信息系统-源程序+mysql+系统+lw文档+远程调试
    基于Echarts实现可视化数据大屏大数据可视化
    Unity 使用宏
  • 原文地址:https://blog.csdn.net/qq_27672101/article/details/117304561