主頁 >  其他 > Jetpact-activity組件完全決議

Jetpact-activity組件完全決議

2020-11-13 12:02:43 其他

Jetpact-activity組件完全決議

文章目錄

  • Jetpact-activity組件完全決議
    • 前言
    • 原始碼決議
      • 構造器
        • Activity停止時取消View還未執行的事件
          • View層處理
            • 移除點擊事件回呼
            • 移除長按事件
        • Activity銷毀時清除ViewMode中資料
          • isChangingConfigurations() 相關
          • getViewModelStore()
          • ViewModelStore.clear()
        • 解決InputMethodManager中 mNextServedView 持有Activity導致記憶體泄漏
      • 對Lifecycle的支持
      • 對fragment回傳鍵調度支持
        • 使用BackPressedDispatcher
        • 回傳呼度原始碼決議
          • OnBackPressedCallback 回呼
            • Cancellable
          • OnBackPressedDispatcher 回傳呼度路由
      • activity-ktx擴展庫功能
        • OnBackPressedDispatcherKt
        • ActivityViewModelLazyKt
    • 總結
    • 參考

前言

Jetpack-activity/fragment 是jetpack架構組件中最基礎的部分,

底層對jetpack-lifecycle組件做了支持、為開發者能夠直接使用jetpack架構組件提供了支持,因此要想徹底了解jetpack系列,先學習activity/fragment組件很有必要,

注意:
本文中源代碼均使用 1.1.0穩定版本

參考

def activity_version = "1.1.0"
// Java language implementation
implementation "androidx.activity:activity:$activity_version"
// Kotlin
implementation "androidx.activity:activity-ktx:$activity_version"

繼承結構:

在這里插入圖片描述

原始碼決議

/**
 * Base class for activities that enables composition of higher level components.
 * <p>
 * Rather than all functionality being built directly into this class, only the minimal set of
 * lower level building blocks are included. Higher level components can then be used as needed
 * without enforcing a deep Activity class hierarchy or strong coupling between components.
 */
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner,
        ViewModelStoreOwner,
        HasDefaultViewModelProviderFactory,
        SavedStateRegistryOwner,
        OnBackPressedDispatcherOwner {
        //....
}        

ComponentActivity 類的注釋上可以得出兩條資訊:

  1. ComponentActivity 是一個支持組合高級組件的基類Activity
  2. 并沒有將所有的組件都構建到這個類中,只是包含最基礎的底層組件,開發者可以根據需要使用更高級別的組件,無需在組件之間強耦合,

構造器

ComponentActivity在構造器中針對Android不同版本進行了簡單兼容處理


public ComponentActivity() {
    Lifecycle lifecycle = getLifecycle();
    //noinspection ConstantConditions
    //如果在使用 Lifecycle 物件的時候還沒有初始化則直接拋錯,對于重寫 getLifecycle() 方法需要注意
    if (lifecycle == null) {
        throw new IllegalStateException("getLifecycle() returned null in ComponentActivity's "
                + "constructor. Please make sure you are lazily constructing your Lifecycle "
                + "in the first call to getLifecycle() rather than relying on field "
                + "initialization.");
    }
    //針對API 19以上兼容
    if (Build.VERSION.SDK_INT >= 19) {
        getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source,
                    @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_STOP) {
                    Window window = getWindow();
                    final View decor = window != null ? window.peekDecorView() : null;
                    if (decor != null) {
                        decor.cancelPendingInputEvents();
                    }
                }
            }
        });
    }
    //Activity銷毀時清除ViewMode中資料
    getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                if (!isChangingConfigurations()) {
                    getViewModelStore().clear();
                }
            }
        }
    });
    //針對 19~23 版本 解決 InputMethodManager中 mNextServedView 記憶體泄漏問題
    if (19 <= SDK_INT && SDK_INT <= 23) {
        getLifecycle().addObserver(new ImmLeaksCleaner(this));
    }
}

從構造器中代碼來看做的事情比較多,但都是兼容性處理,有三個方面的處理:

  • 針對API19以上兼容Activity停止時取消View還未執行的事件
  • 針對API19以上Activity銷毀時清除ViewMode中資料
  • 針對API19~23版本Activity銷毀時解決InputMethodManager中 mNextServedView持有Activity導致記憶體泄漏問題

Activity停止時取消View還未執行的事件

Activity 處于stop 時用戶是看不到界面的,也沒有必要再處理 View的點擊、長按、影片等事件,所以有必要將這些事件移除掉,

我們跟隨原始碼看一下具體是怎么做的:

View層處理

上述代碼會呼叫ViewcancelPendingInputEvents() 這是個取消事件的總調度方法,它沒有具體做事情,而是呼叫了 dispatchCancelPendingInputEvents() 來完成作業

public final void cancelPendingInputEvents() {
    dispatchCancelPendingInputEvents();
}
void dispatchCancelPendingInputEvents() {
    //位操作設定標志位
    mPrivateFlags3 &= ~PFLAG3_CALLED_SUPER;
    //執行清除事件作業
    onCancelPendingInputEvents();
    //檢查標志位是否正確 以確保完成了清除作業
    if ((mPrivateFlags3 & PFLAG3_CALLED_SUPER) != PFLAG3_CALLED_SUPER) {
        throw new SuperNotCalledException("View " + getClass().getSimpleName() +
                " did not call through to super.onCancelPendingInputEvents()");
    }
}

dispatchCancelPendingInputEvents() 方法中呼叫了 onCancelPendingInputEvents() 來完成具體的清除作業:

onCancelPendingInputEvents() 會清除已發送到訊息隊列的事件,延遲事件等 如果是自定義View 是可以重寫此方法,來自定義指定那些事件是需要清除或保留的,但是需要注意要 super.onCancelPendingInputEvents() 要呼叫父類方法 完成 mPrivateFlags3變數的位操作

public void onCancelPendingInputEvents() {
    //移除點擊事件回呼
    removePerformClickCallback();
    //取消等待的長按事件
    cancelLongPress();
    //對標識變數進行操作 在 dispatchCancelPendingInputEvents()對此變數有檢查操作
    mPrivateFlags3 |= PFLAG3_CALLED_SUPER;
}
移除點擊事件回呼

removePerformClickCallback() 中直接呼叫 removeCallbacksmPerformClick點擊事件傳入

 @UnsupportedAppUsage
 private void removePerformClickCallback() {
     if (mPerformClick != null) {
         removeCallbacks(mPerformClick);
     }
 }

removeCallbacks(Runnable action)才是真正移除事件處理的方法,凡是以下幾種方式添加的事件或延遲事件都會移除

  1. post()
  2. postDelayed()
  3. postOnAnimation()
  4. postOnAnimationDelayed()
public boolean removeCallbacks(Runnable action) {
    if (action != null) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            //移除指定回呼
            attachInfo.mHandler.removeCallbacks(action);
            //移除Choreographer中影片回呼
            attachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
                    Choreographer.CALLBACK_ANIMATION, action, null);
        }
        //移除等待佇列中事件
        getRunQueue().removeCallbacks(action);
    }
    return true;
}
移除長按事件

cancelLongPress()中會分別呼叫 removeLongPressCallback() 清除長按回呼 和 removeTapCallback()移除長按產生的超時事件

removeLongPressCallback()removeTapCallback() 都會呼叫 removeCallbacks(Runnable action) 來移除指定的事件,前面我們已經分析過了,就不再贅述了,

public void cancelLongPress() {
    //移除長按回呼事件
    removeLongPressCallback();
    //移除長按超時回呼事件
    removeTapCallback();
}
//移除長按回呼
private void removeLongPressCallback() {
    if (mPendingCheckForLongPress != null) {
        removeCallbacks(mPendingCheckForLongPress);
    }
}
//移除長按超時事件、修改標志位
private void removeTapCallback() {
    if (mPendingCheckForTap != null) {
        mPrivateFlags &= ~PFLAG_PREPRESSED;
        removeCallbacks(mPendingCheckForTap);
    }
}

Activity銷毀時清除ViewMode中資料

Activity銷毀時清除ViewMode中資料,需要依賴另一個組件-Lifecycle支持,不僅是 ViewModel 在Jetpack 架構組件中很多組件都需要依賴 Lifecycle組件,

我們重新看一下以下代碼

//Activity銷毀時清除ViewMode中資料
getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
            @NonNull Lifecycle.Event event) {
        if (event == Lifecycle.Event.ON_DESTROY) {
            if (!isChangingConfigurations()) {
                getViewModelStore().clear();
            }
        }
    }
});

我們看到 !isChangingConfigurations() 為 true ,也就是Activity配置沒有修改情況下,在Activity銷毀時會呼叫 getViewModelStore().clear()

這里我們先將ViewModel放一放,來看一下為什么在清除 ViewModel中資料還有一個前置條件?這個條件什么時候滿足條件?

isChangingConfigurations() 相關

isChangingConfigurations()Activity類中方法,用來判斷 Activity 的配置資訊是否更改了,(比如 橫豎屏切換、語言發生變化等)需要重新啟動該Activity的時候 這個方法會回傳 true 、沒有更改和默認情況都是 false

/** true if the activity is being destroyed in order to recreate it with a new configuration */
/*package*/ boolean mChangingConfigurations = false;

public boolean isChangingConfigurations() {
    return mChangingConfigurations;
}

我們看到 mChangingConfigurations變數是包級訪問權限,我們知道 Activity資源發生變化時會重新啟動,在 framework層經過一系列呼叫,最侄訓呼叫 ActivityThreadhandleRelaunchActivity() 將 mChangingConfigurations 設定為 true

@Override
public void handleRelaunchActivity(ActivityClientRecord tmp,
                                   PendingTransactionActions pendingActions) {
    //.....
    int configChanges = 0;
    ActivityClientRecord r = mActivities.get(tmp.token);
    r.activity.mConfigChangeFlags |= configChanges;
    r.mPreserveWindow = tmp.mPreserveWindow;
    //將標識設定為修改
    r.activity.mChangingConfigurations = true;
    //重新啟動Activity
    handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
            pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
    //.....
}                                                                                                  

所以這就是為什么ViewModel能夠在Activity橫豎屏切換,還能保存資料不丟失的原因,

getViewModelStore()

再回到 ComponentActivity 中我們看一下 getViewModelStore() 是如何實作的

@NonNull
@Override
public ViewModelStore getViewModelStore() {
    if (getApplication() == null) {
        throw new IllegalStateException("Your activity is not yet attached to the "
                + "Application instance. You can't request ViewModel before onCreate call.");
    }
    if (mViewModelStore == null) {
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        //從上一次保存的配置修改中恢復 ViewModelStore 實體       
        if (nc != null) {
            // Restore the ViewModelStore from NonConfigurationInstances
            mViewModelStore = nc.viewModelStore;
        }
        //如果還是null new出一個ViewModelStore實體
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
    return mViewModelStore;
}

我們看到 mViewModelStore是個全域變數,在進行實體化時會先從 上一次保存的 NonConfigurationInstances 物件中恢復,如果為null 最侄訓重新 new 出來一個新的 ViewModelStore 實體并賦值給 mViewModelStore

NonConfigurationInstancesComponentActivity 中的靜態內部類 定義如下

static final class NonConfigurationInstances {
    Object custom;
    ViewModelStore viewModelStore;
}

Activity 非正常銷毀時會觸發 onRetainNonConfigurationInstance() 來保存一些資料,上面 NonConfigurationInstances 類中 viewModelStore 實體就是這樣保存的

@Override
@Nullable
public final Object onRetainNonConfigurationInstance() {
    //取出自定義資料
    Object custom = onRetainCustomNonConfigurationInstance();
    ViewModelStore viewModelStore = mViewModelStore;
    if (viewModelStore == null) {
        // No one called getViewModelStore(), so see if there was an existing
        // ViewModelStore from our last NonConfigurationInstance
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            viewModelStore = nc.viewModelStore;
        }
    }
    if (viewModelStore == null && custom == null) {
        return null;
    }
    //創建NonConfigurationInstances物件保存 自定義資料和 viewModelStore 實體物件
    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.custom = custom;
    nci.viewModelStore = viewModelStore;
    return nci;
}

我們看到 onRetainNonConfigurationInstance() 已被標記為 final,官方不建議我們自己再復寫此方法,而 onRetainCustomNonConfigurationInstance() 和與之對應的 getLastCustomNonConfigurationInstance()也都被標記為廢棄,可以看出官方還沒有提供成熟方案,

@Deprecated
@Nullable
public Object onRetainCustomNonConfigurationInstance() {
    return null;
}

@Deprecated
@Nullable
public Object getLastCustomNonConfigurationInstance() {
    NonConfigurationInstances nc = (NonConfigurationInstances)
            getLastNonConfigurationInstance();
    return nc != null ? nc.custom : null;
}
ViewModelStore.clear()

重新回到 ViewModelStore類的 clear()這里,ViewModelStore類代碼比較簡單,我們著重看一下 clear(),其實就是遍歷 HashMap,并呼叫ViewModel中的clear()

public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
}

ViewModelclear()方法如下:

@MainThread
final void clear() {
    //設定標志位
    mCleared = true;
   
   //清除快取的tag map集合
    if (mBagOfTags != null) {
        synchronized (mBagOfTags) {
            for (Object value : mBagOfTags.values()) {
                // see comment for the similar call in setTagIfAbsent
                closeWithRuntimeException(value);
            }
        }
    }
    //供子類使用清除子類資料
    onCleared();
}

我們看到 clear()一共干了三件事:

  1. 設定清除標志位
  2. 清除快取在HashMap中的tag資料
  3. 呼叫onCleared() 子類可以重寫此方法完成清除資料

解決InputMethodManager中 mNextServedView 持有Activity導致記憶體泄漏

在Android 4.4~6.0之間一直存在一個比較常見的系統bug,那就是 InputMethodManager類中 mNextServedViewactivity銷毀后也會一直持有Activity參考從而導致記憶體泄漏,使用LeakCanary很容易檢測出來

在這里插入圖片描述

常見的解決方式是通過反射得到 InputMethodManagermNextServedViewActivity銷毀后置為null,把參考鏈給斷開 比如可以參考這篇文章傳統解決方式、下面我們看一下 ComponentActivity 是怎么解決這個問題的

前面我們已經在構造器中看到如下代碼:

if (19 <= SDK_INT && SDK_INT <= 23) {
    getLifecycle().addObserver(new ImmLeaksCleaner(this));
}

利用 Lifecyle添加一個觀察者物件,創建了一個 ImmLeaksCleaner并將當前 Activity物件傳入

@RequiresApi(19)
final class ImmLeaksCleaner implements LifecycleEventObserver {
    //變數初始化狀態列舉值
    private static final int NOT_INITIALIAZED = 0;
    private static final int INIT_SUCCESS = 1;
    private static final int INIT_FAILED = 2;
    
    //初始化狀態
    private static int sReflectedFieldsInitialized = NOT_INITIALIAZED;
    
    //反射對應的欄位值
    private static Field sHField;
    private static Field sServedViewField;
    private static Field sNextServedViewField;

    private Activity mActivity;

    ImmLeaksCleaner(Activity activity) {
        mActivity = activity;
    }

    @Override
    public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
        //activity生命周期走到 onDestory() 時才會往下執行
        if (event != Lifecycle.Event.ON_DESTROY) {
            return;
        }
        //發現沒有初始化進行初始化反射出指定欄位
        if (sReflectedFieldsInitialized == NOT_INITIALIAZED) {
            initializeReflectiveFields();
        }
        //反射成功
        if (sReflectedFieldsInitialized == INIT_SUCCESS) {
            //獲取當前InputMethodManager物件
            InputMethodManager inputMethodManager = (InputMethodManager)
                    mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
            //拿到當前lock鎖物件
            final Object lock;
            try {
                lock = sHField.get(inputMethodManager);
            } catch (IllegalAccessException e) {
                return;
            }
            if (lock == null) {
                return;
            }
            //進入同步鎖
            synchronized (lock) {
                final View servedView;
                try {
                    servedView = (View) sServedViewField.get(inputMethodManager);
                } catch (IllegalAccessException e) {
                    return;
                } catch (ClassCastException e) {
                    return;
                }
                if (servedView == null) {
                    return;
                }
                if (servedView.isAttachedToWindow()) {
                    return;
                }
                //將mNextServedView物件設定為null
                try {
                    sNextServedViewField.set(inputMethodManager, null);
                } catch (IllegalAccessException e) {
                    return;
                }
            }
            inputMethodManager.isActive();
        }
    }

    @MainThread
    private static void initializeReflectiveFields() {
        try {
            //設定標識位標識開始反射
            sReflectedFieldsInitialized = INIT_FAILED;
            sServedViewField = InputMethodManager.class.getDeclaredField("mServedView");
            sServedViewField.setAccessible(true);
            sNextServedViewField = InputMethodManager.class.getDeclaredField("mNextServedView");
            sNextServedViewField.setAccessible(true);
            //對應是Handler實作類
            sHField = InputMethodManager.class.getDeclaredField("mH");
            sHField.setAccessible(true);
            //反射成功重置標識位
            sReflectedFieldsInitialized = INIT_SUCCESS;
        } catch (NoSuchFieldException e) {
            // very oem much custom ˉ\_(ツ)_/ˉ
        }
    }
}

以上就是 ImmLeaksCleaner類解決方式,代碼比較簡單有詳細的注釋,就不再贅述了


對Lifecycle的支持

我們上面看到 ComponentActivity實作了 LifecycleOwner介面,內部創建了 LifecycleRegistry 物件并將當前Activity實體傳入

//創建 LifecycleRegistry 物件
private final LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);

getLifecycle()回傳的值即 mLifecycleRegistry

@NonNull
@Override
public Lifecycle getLifecycle() {
    return mLifecycleRegistry;
}

getLifecycle()實作很簡單,就是將new出來的mLifecycleRegistry回傳,我們從getLifecycle() 的注釋上我們提取到兩點資訊:

  • 官方不推薦重寫 getLifecycle() 而且會在未來高版本中會將此方法標記為 final
  • 如果你想重寫 getLifecycle() 就需要遵循以下兩條
    1. 必須回傳一個 LifecycleRegistry 物件
    2. LifecycleRegistry 物件進行懶初始化
      注意:在LifecycleRegistry 物件初始化完成之前,這個物件將會在父類的構造器中呼叫

對fragment回傳鍵調度支持

什么是對fragment回傳鍵的調度支持? 其本質就是讓fragment 能像Activity一樣在按下回傳鍵時能夠回呼onBackPressed() 所以BackPressedDispatcher調度器本質也是將onBackPressed()回呼到fragment里面實作而已

下面我們先看一個在fragment里面具體如何使用回傳呼度?

使用BackPressedDispatcher

1.創建Activity

第一步創建一個測驗Activity內部布局和相關代碼如下:

class BackMainActivity : BaseEasyActivity() {


    override fun getLayoutId(): Int {
        return R.layout.activity_jetpack_back_main
    }

    override fun initView() {

        supportFragmentManager.beginTransaction()
            .replace(R.id.backContent, BackListFragment())
            .commitNowAllowingStateLoss()

    }

    override fun onBackPressed() {
        super.onBackPressed()
        Logger.d("onBackPressed")
    }
}

activity_jetpack_back_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/backContent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>

BackListFragment:

class BackListFragment : BaseEasyListFragment() {


    override fun initView() {
        super.initView()

        requireActivity().onBackPressedDispatcher
            .addCallback(this, object : OnBackPressedCallback(true) {
                override fun handleOnBackPressed() {
                    Logger.i("back 1")
                    back()
                }
            })
        requireActivity().onBackPressedDispatcher.addCallback(this,object :OnBackPressedCallback(true){
            override fun handleOnBackPressed() {
                Logger.i("back 2")

                back2()
            }
        })


    }

    private fun back2() {
        activity?.let {
            AlertDialog.Builder(it).setTitle("EasyTitle 2")
                .setMessage("你確定退出嗎?")
                .setNegativeButton(
                    "取消"
                ) { dialog, _ ->
                    dialog?.dismiss()
                }
                .setPositiveButton(
                    "確定"
                ) { dialog, _ ->
                    dialog?.dismiss()
                    requireActivity().finish()
                }
                .create()
                .show()
        }

    }

    private fun back() {
        activity?.let {
            AlertDialog.Builder(it).setTitle("EasyTitle 1")
                .setMessage("你確定退出嗎?")
                .setNegativeButton(
                    "取消"
                ) { dialog, _ ->
                    dialog?.dismiss()
                }
                .setPositiveButton(
                    "確定"
                ) { dialog, _ ->
                    dialog?.dismiss()
                    requireActivity().finish()
                }
                .create()
                .show()
        }
    }
}

fragment里面我們呼叫requireActivity().onBackPressedDispatcher.addCallback()添加了兩個回呼,并在handleOnBackPressed()回呼中我們彈出一個確認彈框

addCallback() 這個方法有兩個引數含義分別是:

  1. @NonNull LifecycleOwner owner : 當前的lifecycle實作物件 ActiivtyFragment頂級類都實作了LifecycleOwner介面,所以第一個引數一般傳入this就可以了
  2. @NonNull OnBackPressedCallback onBackPressedCallback : OnBackPressedCallback接識訓傳鍵回呼抽象類,子類需要繼承此類,其中構造方法中的 boolean enabled 引數必須傳入 true如果 傳入 false此回呼不會執行,默認值為 false

以上就是我們Demo全部代碼了,當我們運行程式,點擊回傳鍵 我們看到 back2()里的彈框顯示出來了,點擊確定按鈕將會呼叫finish()關閉當前頁面

你可能會疑問我們注冊了兩個回呼,但是back()彈框并沒有顯示,是怎么回事呢?那就只能看一下原始碼才能知道答案

回傳呼度原始碼決議

回傳鍵調度代碼的源頭還是在 ComponentActivity中,讓我們重新將注意力轉移到此類中,前文中我們看到 ComponentActivity實作的介面有一個 OnBackPressedDispatcherOwner :

public interface OnBackPressedDispatcherOwner extends LifecycleOwner {

    /**
     * Retrieve the {@link OnBackPressedDispatcher} that should handle the system back button.
     *
     * @return The {@link OnBackPressedDispatcher}.
     */
    @NonNull
    OnBackPressedDispatcher getOnBackPressedDispatcher();
}

可以看到 OnBackPressedDispatcherOwner繼承與 LifecycleOwner 那么他自然也擁有lifecycle相關的功能 getOnBackPressedDispatcher() 是回傳一個回傳鍵路由類,這個類會將系統回傳鍵觸發路由到指定回呼中

下面我們看一下 ComponentActivity類回傳鍵路由相關代碼和實作邏輯

private final OnBackPressedDispatcher mOnBackPressedDispatcher =
        new OnBackPressedDispatcher(new Runnable() {
            @Override
            public void run() {
                ComponentActivity.super.onBackPressed();
            }
        });

@Override
@MainThread
public void onBackPressed() {
    mOnBackPressedDispatcher.onBackPressed();
}

@NonNull
@Override
public final OnBackPressedDispatcher getOnBackPressedDispatcher() {
    return mOnBackPressedDispatcher;
}        
  • ComponentActivity創建出回傳鍵路由類,并傳入了一個默認任務,run()中并將此次點擊回傳鍵任務交由父類來實作,這個默認邏輯只有在不存在任何自定義回呼的情況下執行
  • onBackPressed()中就是將任務交由 OnBackPressedDispatcher來執行
  • getOnBackPressedDispatcher()只是將當前創建出來的實體進行回傳,不過這個方法被標記為 final
OnBackPressedCallback 回呼

下面看一下 OnBackPressedCallback的具體實作

public abstract class OnBackPressedCallback {
    
    private boolean mEnabled;
    
    //存盤Cancellable介面集合
    private CopyOnWriteArrayList<Cancellable> mCancellables = new CopyOnWriteArrayList<>();
    
    public OnBackPressedCallback(boolean enabled) {
        mEnabled = enabled;
    }
    
    @MainThread
    public final void setEnabled(boolean enabled) {
        mEnabled = enabled;
    }
    
    @MainThread
    public final boolean isEnabled() {
        return mEnabled;
    }
    
    //呼叫所有 Cancellable的cancel()函式
    @MainThread
    public final void remove() {
        for (Cancellable cancellable: mCancellables) {
            cancellable.cancel();
        }
    }
    
    //子類需要實作的回傳鍵邏輯
    @MainThread
    public abstract void handleOnBackPressed();
    
    //添加和移除 Cancellable 的方法,主要是組件庫代碼內部呼叫(包訪問權限)
    void addCancellable(@NonNull Cancellable cancellable) {
        mCancellables.add(cancellable);
    }
    void removeCancellable(@NonNull Cancellable cancellable) {
        mCancellables.remove(cancellable);
    }
}

上述OnBackPressedCallback 代碼邏輯比較簡單,有比較詳細的注釋就不再贅述了

Cancellable

Cancellable 是一個組件庫代碼內部(包訪問權限),取消介面定義如下:

interface Cancellable {

    /**
     * Cancel the subscription. This call should be idempotent, making it safe to
     * call multiple times.
     */
    void cancel();
}
OnBackPressedDispatcher 回傳呼度路由

下面我們看一下 OnBackPressedDispatcher 類具體實作

public final class OnBackPressedDispatcher {

    //默認回傳任務
    @Nullable
    private final Runnable mFallbackOnBackPressed;

    //回傳鍵任務佇列
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final ArrayDeque<OnBackPressedCallback> mOnBackPressedCallbacks = new ArrayDeque<>();

    //默認、有參構造器
    public OnBackPressedDispatcher() {
        this(null);
    }
    public OnBackPressedDispatcher(@Nullable Runnable fallbackOnBackPressed) {
        mFallbackOnBackPressed = fallbackOnBackPressed;
    }

    //添加回傳鍵任務
    @MainThread
    public void addCallback(@NonNull OnBackPressedCallback onBackPressedCallback) {
        //呼叫addCancellableCallback()下面方法,將回傳任務包裝成可需要性質的任務,
        //子類可以呼叫 OnBackPressedCallback中remove() 將此任務移除掉
        addCancellableCallback(onBackPressedCallback);
    }
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    @MainThread
    @NonNull
    Cancellable addCancellableCallback(@NonNull OnBackPressedCallback onBackPressedCallback) {
        //添加到全域集合中
        mOnBackPressedCallbacks.add(onBackPressedCallback);
        //將普通任務包裝成可需要性質的任務
        OnBackPressedCancellable cancellable = new OnBackPressedCancellable(onBackPressedCallback);
        onBackPressedCallback.addCancellable(cancellable);
        return cancellable;
    }
    
    //添加任務,并指定了lifecycle物件
    @SuppressLint("LambdaLast")
    @MainThread
    public void addCallback(@NonNull LifecycleOwner owner,
            @NonNull OnBackPressedCallback onBackPressedCallback) {
        Lifecycle lifecycle = owner.getLifecycle();
        //不能在 DESTROYED 狀態時注冊
        if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
            return;
        }
        //添加一個LifecycleOnBackPressedCancellable 具有生命周期觀察能力,可需要性質的任務
        onBackPressedCallback.addCancellable(
                new LifecycleOnBackPressedCancellable(lifecycle, onBackPressedCallback));
    }

    //判斷是否存在打開的回呼任務
    @MainThread
    public boolean hasEnabledCallbacks() {
        Iterator<OnBackPressedCallback> iterator =
                mOnBackPressedCallbacks.descendingIterator();
        while (iterator.hasNext()) {
            if (iterator.next().isEnabled()) {
                return true;
            }
        }
        return false;
    }

    //ComponentActivity 類中onBackPressed()會代理到這個方法里執行 
    @MainThread
    public void onBackPressed() {
        //倒序遍歷
        Iterator<OnBackPressedCallback> iterator =
                mOnBackPressedCallbacks.descendingIterator();
        while (iterator.hasNext()) {
            OnBackPressedCallback callback = iterator.next();
            //如果OnBackPressedCallback中mEnabled值為 true才會執行
            //且只會執行任務佇列中第一個任務,所以一個fragment如果添加多個任務,只會執行最后添加的任務
            if (callback.isEnabled()) {
                callback.handleOnBackPressed();
                return;
            }
        }
        //上述任務佇列中沒有找到可執行的自定義任務,則會將此次事件交給ComponentActivity來執行 
        if (mFallbackOnBackPressed != null) {
            mFallbackOnBackPressed.run();
        }
    }
    
    //對普通回傳任務進行包裝成可取消性質的
    private class OnBackPressedCancellable implements Cancellable {
        private final OnBackPressedCallback mOnBackPressedCallback;
        OnBackPressedCancellable(OnBackPressedCallback onBackPressedCallback) {
            mOnBackPressedCallback = onBackPressedCallback;
        }

        @Override
        public void cancel() {
            //從佇列中移除和移除自身回呼
            mOnBackPressedCallbacks.remove(mOnBackPressedCallback);
            mOnBackPressedCallback.removeCancellable(this);
        }
    }

    //對指定Lifecycle實作類進行包裝,內部自動處理生命周期相關狀態
    private class LifecycleOnBackPressedCancellable implements LifecycleEventObserver,
            Cancellable {
        private final Lifecycle mLifecycle;
        private final OnBackPressedCallback mOnBackPressedCallback;

        @Nullable
        private Cancellable mCurrentCancellable;

        LifecycleOnBackPressedCancellable(@NonNull Lifecycle lifecycle,
                @NonNull OnBackPressedCallback onBackPressedCallback) {
            mLifecycle = lifecycle;
            mOnBackPressedCallback = onBackPressedCallback;
            //添加lifecycle監聽
            lifecycle.addObserver(this);
        }

        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            if (event == Lifecycle.Event.ON_START) {
                //在fragment啟動的時候將任務添加進去,并將任務包裝成可需要的任務
                mCurrentCancellable = addCancellableCallback(mOnBackPressedCallback);
            } else if (event == Lifecycle.Event.ON_STOP) {
                
                //在fragment stop的時候取消任務
                if (mCurrentCancellable != null) {
                    mCurrentCancellable.cancel();
                }
            } else if (event == Lifecycle.Event.ON_DESTROY) {
                //fragment銷毀時將任務取消
                cancel();
            }
        }

        @Override
        public void cancel() {
            //移除lifecycle回呼
            mLifecycle.removeObserver(this);
            //移除回呼
            mOnBackPressedCallback.removeCancellable(this);
            if (mCurrentCancellable != null) {
                mCurrentCancellable.cancel();
                mCurrentCancellable = null;
            }
        }
    }
}

以上就是OnBackPressedDispatcher回傳呼度路由類的全部代碼,有詳細的注釋應該看明白,

OnBackPressedDispatcher 是實作回傳呼度人主要類,內部處理了添加任務,移除任務,將任務路由到指定的 fragment中,這里在添加任務時 推薦使用addCallback(@NonNull LifecycleOwner owner, @NonNull OnBackPressedCallback onBackPressedCallback) 來添加任務,這樣就能和 lifecycle關聯起來,內部已經處理了和fragment生命周期相關的邏輯了,

activity-ktx擴展庫功能

activity-ktx擴展庫是 Google 使用kotlin語言開發的輔助庫,同時支持了kotlin協程,對于使用jetpack庫很有幫助

activity-ktx擴展庫主要包含兩個kotlin檔案:

  • OnBackPressedDispatcherKt
  • ActivityViewModelLazyKt

OnBackPressedDispatcherKt

是專門對 OnBackPressedDispatcher類的一個擴展和包裝來看看具體怎么做的

//繼承OnBackPressedCallback并對 OnBackPressedDispatcher的addCallback()進行擴展
fun OnBackPressedDispatcher.addCallback(
    owner: LifecycleOwner? = null,
    enabled: Boolean = true,
    onBackPressed: OnBackPressedCallback.() -> Unit
): OnBackPressedCallback {
    //內部實作類
    val callback = object : OnBackPressedCallback(enabled) {
        override fun handleOnBackPressed() {
            //執行傳過來的函式式方法
            onBackPressed()
        }
    }
    //對LifecycleOwner不同情況呼叫不同API
    if (owner != null) {
        addCallback(owner, callback)
    } else {
        addCallback(callback)
    }
    return callback
}

從上述原始碼中我們看到這個方法功能還是比較多的,再具體使用時就比較方便了

使用:

//添加回傳回呼
requireActivity().onBackPressedDispatcher.addCallback(owner = this,enabled = true,{
    //...
})

當然根據kotlin具名函式的特點,也可以省略前兩個引數:

requireActivity().onBackPressedDispatcher.addCallback(onBackPressed = {
    //...
})

所以在使用上比之前的方式要簡單很多

ActivityViewModelLazyKt

這個擴展類是為了幫助我們方便的使用ViewModel類,想想一下我們是如何創建ViewModel

這里先創建出來一個自定義ViewModel,看一下有多少種方式創建實體

class BackViewModel(application: Application) : AndroidViewModel(application) {}

ViewModelProviders 方式

viewModel = ViewModelProviders.of(this).get(BackViewModel::class.java)

使用ViewModelProviders呼叫of()并呼叫get()就能創建實體,很方便的,但是很不幸在后來的版本中Google 先是將 ViewModelProviders標記為過時,再后來就直接洗掉了

所以Google推薦直接使用ViewModelProvider來創建實體,其實 ViewModelProvidersof()get()也是對 ViewModelProvider的簡單封裝

ViewModelProvider 方式

val viewModel = ViewModelProvider(
    this,
    ViewModelProvider.AndroidViewModelFactory.getInstance(application)
).get(BackViewModel::class.java)

看著是有點麻煩哈…不過代碼邏輯還是很好懂的

viewModels() 方式

下面看看利用 ActivityViewModelLazyKt 擴展組件創建ViewModel實體

只需要呼叫 viewModels() 函式就可以了

val backViewModel = viewModels<BackViewModel> {
    ViewModelProvider.AndroidViewModelFactory.getInstance(application)
}

當然上面方式是有點麻煩,還需要傳入一個 lambda 運算式感徑訓不好理解,不過使用默認的 ViewModelFactory 就比較簡單了

val viewModel: BackViewModel by viewModels()

或者這樣寫

val viewModel by viewModels<BackViewModel>()

不過意思是一樣的相對以上方式就簡單很多了,幾乎感受不到 ViewModelProviderAndroidViewModelFactory 等類的存在

接下來看一下viewModels()是如何實作的

viewModels()簡要原始碼分析

//實作了Lazy介面具備懶加載欄位的功能
//ComponentActivity類的擴展函式
@MainThread
inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
    noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    //①
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }
    //②
    return ViewModelLazy(VM::class, {
        viewModelStore
    }, factoryPromise)
}

上述代碼雖然簡短但是功能比較多,我們具體看一下

  1. 根據引數是否為null來選擇是使用自定義 ViewModelProviderFactory 還是 默認的 ViewModelProviderFactory , defaultViewModelProviderFactory 變數對應是 mDefaultFactory (SavedStateViewModelFactory型別)
  2. 根據引數構建一個 ViewModelLazy物件回傳

注意: 原始碼中有如下注釋

This property can be accessed only after the Activity is attached to the Application,
and access prior to that will result in IllegalArgumentException.

這里的意思是,如果使用擴展函式初始化的屬性只能在Actiivty添加了Application后才能訪問,在此之前的訪問將會拋出IllegalArgumentException例外

我們知道在啟動Activity 會呼叫 Activityattach()Application背景關系物件賦予Activity上,所以我們應該保證變數不能在 ActivityonCreate()之前呼叫就可以了

總結

本文主要以 jetpack-activity 組件為切入點分析了該組件的主要功能,并根據原始碼了解了內部實作原理,
簡單來說 jetpack-activity 組件有如下功能:

  • 解決一些Android碎片化適配問題
  • Lifecycle系列組件提供了支持
  • 提供了回傳鍵路由,對Fragment處理回傳鍵提供了支持

同時 ktx 擴展組件也是對jetpack-activity 組件庫的一個補充,在其他的組件庫中 ktx 更是比較重要,

參考

https://developer.android.com/jetpack

https://developer.android.com/jetpack/androidx/releases/activity

https://www.jianshu.com/p/f2aa4cf53abd

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/213307.html

標籤:其他

上一篇:IDEA 常用的一些 (就幾個) 快捷鍵

下一篇:Android 實作HTTPS自簽名證書(非常詳細)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more