android面试之fragment,当你面试的时候,被问到关于Fragment的种种
前言
不知道你們都沒(méi)有自己特別的學(xué)習(xí)的方法,我是有吧所有的整理成筆記的習(xí)慣
比如今天講解的關(guān)于Fragment的我會(huì)做成筆記
由于文章有些地方代碼過(guò)于太長(zhǎng)了繁瑣,所以部分省略掉了,敲了一下午眼睛和手脖子都酸了,至于省略的部分,對(duì)這些筆記,面試內(nèi)容感興趣的可以看筆記研究,歡迎留言
相關(guān)內(nèi)容后續(xù)GitHub更新,想沖擊金三銀四的小伙伴可以找找看看,歡迎star
(順手留下GitHub鏈接,需要獲取相關(guān)面試等內(nèi)容的可以自己去找)
https://github.com/xiangjiana/Android-MS
一丶Fragment 的使用
實(shí)現(xiàn)很簡(jiǎn)單,創(chuàng)建一個(gè)的布局,然后在 Activity 里點(diǎn)擊時(shí)替換 Fragment
mFragmentManager = getSupportFragmentManager();
mFragmentManager.beginTransaction()
.replace(R.id.fl_content, fragment)
.commitAllowingStateLoss();
代碼很簡(jiǎn)單,核心就三步:
創(chuàng)建 Fragment
獲取 FragmentManager
調(diào)用事務(wù),添加、替換
我們一步步來(lái)了解這背后的故事。
Fragment 大家應(yīng)該比較熟悉,放到最后。
先來(lái)看看 FragmentManager 。
####二丶 FragmentManager
public abstract class FragmentManager {...}
FragmentManager 是一個(gè)抽象類,定義了一些和 Fragment 相關(guān)的操作和內(nèi)部類/接口。
2.1.定義的操作
FragmentManager 中定義的方法如下:
//開(kāi)啟一系列對(duì) Fragments 的操作
public abstract FragmentTransaction beginTransaction();
//FragmentTransaction.commit() 是異步執(zhí)行的,如果你想立即執(zhí)行,可以調(diào)用這個(gè)方法
public abstract boolean executePendingTransactions();
//根據(jù) ID 找到從 XML 解析出來(lái)的或者事務(wù)中添加的 Fragment
//首先會(huì)找添加到 FragmentManager 中的,找不到就去回退棧里找
public abstract Fragment findFragmentById(@IdRes int id);
//跟上面的類似,不同的是使用 tag 進(jìn)行查找
public abstract Fragment findFragmentByTag(String tag);
//彈出回退棧中棧頂?shù)?Fragment,異步執(zhí)行的
public abstract void popBackStack();
//立即彈出回退棧中棧頂?shù)?#xff0c;直接執(zhí)行哦
public abstract boolean popBackStackImmediate();
......
可以看到,定義的方法有很多是異步執(zhí)行的,后面看看它究竟是如何實(shí)現(xiàn)的異步。
2.2.內(nèi)部類/接口:
BackStackEntry:Fragment 后退棧中的一個(gè)元素
onBackStackChangedListener:后退棧變動(dòng)監(jiān)聽(tīng)器
FragmentLifecycleCallbacks: FragmentManager 中的 Fragment 生命周期監(jiān)聽(tīng)
//后退棧中的一個(gè)元素
public interface BackStackEntry {
//棧中該元素的唯一標(biāo)識(shí)
public int getId(); //獲取 FragmentTransaction#addToBackStack(String) 設(shè)置的名稱 public String getName();
@StringRes
public int getBreadCrumbTitleRes();
@StringRes
public int getBreadCrumbShortTitleRes();
public CharSequence getBreadCrumbTitle();
public CharSequence getBreadCrumbShortTitle();
}
可以看到 BackStackEntry 的接口比較簡(jiǎn)單,關(guān)鍵信息就是 ID 和 Name。
//在 Fragment 回退棧中有變化時(shí)回調(diào)
public interface OnBackStackChangedListener {
public void onBackStackChanged();
}
//FragmentManager 中的 Fragment 生命周期監(jiān)聽(tīng)
public abstract static class FragmentLifecycleCallbacks {
public void onFragmentPreAttached(FragmentManager fm, Fragment f, Context context) {}
public void onFragmentAttached(FragmentManager fm, Fragment f, Context context) {}
public void onFragmentCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) {}
public void onFragmentActivityCreated(FragmentManager fm, Fragment f, Bundle savedInstanceState) {}
public void onFragmentViewCreated(FragmentManager fm, Fragment f, View v, Bundle savedInstanceState) {}
public void onFragmentStarted(FragmentManager fm, Fragment f) {}
public void onFragmentResumed(FragmentManager fm, Fragment f) {}
public void onFragmentPaused(FragmentManager fm, Fragment f) {}
public void onFragmentStopped(FragmentManager fm, Fragment f) {}
public void onFragmentSaveInstanceState(FragmentManager fm, Fragment f, Bundle outState) {}
public void onFragmentViewDestroyed(FragmentManager fm, Fragment f) {}
public void onFragmentDestroyed(FragmentManager fm, Fragment f) {}
public void onFragmentDetached(FragmentManager fm, Fragment f) {}
}
}
熟悉 Fragment 生命周期的同學(xué)一定覺(jué)得很面熟,這個(gè)接口就是為我們提供一個(gè) FragmentManager 所 有 Fragment 生命周期變化的回調(diào)。
小結(jié):
可以看到, FragmentManager 是一個(gè)抽象類,它定義了對(duì)一個(gè) Activity/Fragment 中 添加進(jìn)來(lái)的Fragment 列表、Fragment 回退棧的操作、管理。
2.3.實(shí)現(xiàn)類 FragmentManagerImpl
FragmentManager 定義的任務(wù)是由 FragmentManagerImpl 實(shí)現(xiàn)的。
主要成員:
final class FragmentManagerImpl extends FragmentManager implements
LayoutInflaterFactory {
ArrayList mPendingActions;
Runnable[] mTmpActions;
boolean mExecutingActions;
ArrayList mActive;
ArrayList mAdded;
ArrayList mAvailIndices;
ArrayList mBackStack;
ArrayList mCreatedMenus;
// Must be accessed while locked.
ArrayList mBackStackIndices;
ArrayList mAvailBackStackIndices;
ArrayList mBackStackChangeListeners;
private CopyOnWriteArrayList> mLifecycleCallbacks;
//...
}
可以看到, FragmentManagerImpl 中定義了 添加的、活躍的。以及回退棧的列表,這和FragmentManager 的要求一致
接著還有當(dāng)前的狀態(tài),當(dāng)前 Fragment 的起始 mParent,以及 FragmentManager 的 mHost 和mContainer。
FragmentContainer 就是一個(gè)接口,定義了關(guān)于布局的兩個(gè)方法:
public abstract class FragmentContainer {
@Nullable
public abstract View onFindViewById(@IdRes int id);
public abstract boolean onHasView();
}
而 FragmentHostCallback 就復(fù)雜一點(diǎn)了,它提供了 Fragment 需要的信息,也定義了 Fragment 宿主應(yīng)該做的操作:
public abstract class FragmentHostCallback extends FragmentContainer {
private final Activity mActivity;
final Context mContext;
private final Handler mHandler;
final int mWindowAnimations;
final FragmentManagerImpl mFragmentManager = new FragmentManagerImpl();
//...
}
我們知道,一般來(lái)說(shuō) Fragment 的宿主就兩種:
Activity
Fragment
比如 FragmentActivity 的內(nèi)部類 HostCallbacks 就實(shí)現(xiàn)了這個(gè)抽象類:
class HostCallbacks extends FragmentHostCallback {
public HostCallbacks() {
super(FragmentActivity.this /*fragmentActivity*/);
}
//...
@Override
public LayoutInflater onGetLayoutInflater() {
return
FragmentActivity.this.getLayoutInflater().cloneInContext(FragmentActivity.t his);
}
@Override
public FragmentActivity onGetHost() {
return FragmentActivity.this;
}
......
}
我們?cè)倏纯此麑?duì) FragmentManager 定義的關(guān)鍵方法是如何實(shí)現(xiàn)的。
@Override
public FragmentTransaction beginTransaction() {
return new BackStackRecord(this);
}
beginTransaction() 返回一個(gè)新的 BackStackRecord ,我們后面介紹。前面提到了, popBackStack() 是一個(gè)異步操作,它是如何實(shí)現(xiàn)異步的呢?
@Override
public void popBackStack() {
enqueueAction(new PopBackStackState(null, -1, 0), false);
}
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
if (!allowStateLoss) {
checkStateLoss();
}
synchronized (this) {
if (mDestroyed || mHost == null) {
throw new IllegalStateException("Activity has been destroyed");
}
if (mPendingActions == null) {
mPendingActions = new ArrayList<>();
}
mPendingActions.add(action);
scheduleCommit();
}
}
private void scheduleCommit() {
synchronized (this) {
boolean postponeReady = mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit);
}
}
}
可以看到,調(diào)用到最后,是調(diào)用宿主中的 Handler來(lái)發(fā)送任務(wù)的,so easy 嘛。其他的異步執(zhí)行也是類似,就不贅述了。
后退棧相關(guān)方法:
ArrayList mBackStack;
@Override
public int getBackStackEntryCount() {
return mBackStack != null ? mBackStack.size() : 0;
}
@Override
public BackStackEntry getBackStackEntryAt(int index) {
return mBackStack.get(index);
}
可以看到,開(kāi)始事務(wù)和后退棧,返回/操作的都是 BackStackRecord ,我們來(lái)了解了解它是何方神圣。
三丶事務(wù)
BackStackRecord 繼承了 FragmentTransaction :
final class BackStackRecord extends FragmentTransaction implements
FragmentManager.BackStackEntry, FragmentManagerImpl.OpGenerator {...}
先來(lái)看看 FragmentTransaction 。
3.1.FragmentTransaction
FragmentTransaction 定義了一系列對(duì) Fragment 的操作方法:
//它會(huì)調(diào)用 add(int, Fragment, String),其中第一個(gè)參數(shù)傳的是 0
public abstract FragmentTransaction add(Fragment fragment, String tag);
//它會(huì)調(diào)用 add(int, Fragment, String),其中第三個(gè)參數(shù)是 null
public abstract FragmentTransaction add(@IdRes int containerViewId, Fragment fragment);
//添加一個(gè) Fragment 給 Activity 的最終實(shí)現(xiàn)
//第一個(gè)參數(shù)表示 Fragment 要放置的布局 id
//第二個(gè)參數(shù)表示要添加的 Fragment,【注意】一個(gè) Fragment 只能添加一次
//第三個(gè)參數(shù)選填,可以給 Fragment 設(shè)置一個(gè) tag,后續(xù)可以使用這個(gè) tag 查詢它
public abstract FragmentTransaction add(@IdRes int containerViewId, Fragment fragment, @Nullable String tag);
//調(diào)用 replace(int, Fragment, String),第三個(gè)參數(shù)傳的是 null
public abstract FragmentTransaction replace(@IdRes int containerViewId, Fragment fragment);
//替換宿主中一個(gè)已經(jīng)存在的 fragment
//這一個(gè)方法等價(jià)于先調(diào)用 remove(), 再調(diào)用 add() public abstract FragmentTransaction replace(@IdRes int containerViewId, Fragment fragment, @Nullable String tag);
//移除一個(gè)已經(jīng)存在的 fragment
//如果之前添加到宿主上,那它的布局也會(huì)被移除
public abstract FragmentTransaction remove(Fragment fragment);
//隱藏一個(gè)已存的 fragment
//其實(shí)就是將添加到宿主上的布局隱藏
public abstract FragmentTransaction hide(Fragment fragment);
//顯示前面隱藏的 fragment,這只適用于之前添加到宿主上的 fragment
public abstract FragmentTransaction show(Fragment fragment);
//將指定的 fragment 將布局上解除
//當(dāng)調(diào)用這個(gè)方法時(shí),fragment 的布局已經(jīng)銷毀了
public abstract FragmentTransaction detach(Fragment fragment);
//當(dāng)前面解除一個(gè) fragment 的布局綁定后,調(diào)用這個(gè)方法可以重新綁定
//這將導(dǎo)致該 fragment 的布局重建,然后添加、展示到界面上
public abstract FragmentTransaction attach(Fragment fragment);
對(duì) fragment 的操作基本就這幾步,我們知道,要完成對(duì) fragment 的操作,最后還需要提交一下:
mFragmentManager.beginTransaction()
.replace(R.id.fl_child, getChildFragment())
// .commit()
.commitAllowingStateLoss();
2.2.事務(wù)的四種提交方式
事務(wù)最終的提交方法有四種:
commit()
commitAllowingStateLoss()
commitNow()
commitNowAllowingStateLoss()
它們之間的特點(diǎn)及區(qū)別如下:
public abstract int commit();
commit() 在主線程中異步執(zhí)行,其實(shí)也是 Handler 拋出任務(wù),等待主線程調(diào)度執(zhí)行。
注意:
commit() 需要在宿主 Activity 保存狀態(tài)之前調(diào)用,否則會(huì)報(bào)錯(cuò)。
這是因?yàn)槿绻?Activity 出現(xiàn)異常需要恢復(fù)狀態(tài),在保存狀態(tài)之后的 commit() 將會(huì)丟失,這和調(diào)用的初衷不符,所以會(huì)報(bào)錯(cuò)。
public abstract int commitAllowingStateLoss();
commitAllowingStateLoss() 也是異步執(zhí)行,但它的不同之處在于,允許在 Activity 保存狀態(tài)之后調(diào)用,也就是說(shuō)它遇到狀態(tài)丟失不會(huì)報(bào)錯(cuò)。
因此我們一般在界面狀態(tài)出錯(cuò)是可以接受的情況下使用它。
public abstract void commitNow();
commitNow() 是同步執(zhí)行的,立即提交任務(wù)。
前面提到 FragmentManager.executePendingTransactions() 也可以實(shí)現(xiàn)立即提交事務(wù)。但我們一般建議使用 commitNow() , 因?yàn)榱硗饽俏皇且幌伦訄?zhí)行所有待執(zhí)行的任務(wù),可能會(huì)把當(dāng)前所有的事務(wù)都一下子執(zhí)行了,這有可能有副作用。
此外,這個(gè)方法提交的事務(wù)可能不會(huì)被添加到 FragmentManger 的后退棧,因?yàn)槟氵@樣直接提交,有可能影響其他異步執(zhí)行任務(wù)在棧中的順序。
和 commit() 一樣, commitNow() 也必須在 Activity 保存狀態(tài)前調(diào)用,否則會(huì)拋異常。
public abstract void commitNowAllowingStateLoss();
同步執(zhí)行的 commitAllowingStateLoss() 。
OK,了解了 FragmentTransaction 定義的操作,去看看我們真正關(guān)心的、 beginTransaction()中返回的 BackStackRecord :
@Override
public FragmentTransaction beginTransaction() {
return new BackStackRecord(this);
}
3.3事務(wù)真正實(shí)現(xiàn)/回退棧 BackStackRecord
BackStackRecord 既是對(duì) Fragment 進(jìn)行操作的事務(wù)的真正實(shí)現(xiàn),也是 FragmentManager 中的回退棧的實(shí)現(xiàn):
final class BackStackRecord extends
FragmentTransaction implements FragmentManager.BackStackEntry, FragmentManagerImpl.OpGenerator {...}
它的關(guān)鍵成員:
final FragmentManagerImpl mManager;
//Op 可選的狀態(tài)值
static final int OP_NULL = 0;
static final int OP_ADD = 1;
static final int OP_REPLACE = 2;
static final int OP_REMOVE = 3;
static final int OP_HIDE = 4;
static final int OP_SHOW = 5;
static final int OP_DETACH = 6;
static final int OP_ATTACH = 7;
ArrayList mOps = new ArrayList<>();
static final class Op {
int cmd; //狀態(tài)
Fragment fragment;
int enterAnim;
int exitAnim;
int popEnterAnim;
int popExitAnim;
}
int mIndex = -1;
//棧中最后一個(gè)元素的索引
}
可以看到 Op 就是添加了狀態(tài)和動(dòng)畫信息的 Fragment, mOps就是棧中所有的 Fragment。事務(wù)定義的方法它是如何實(shí)現(xiàn)的呢
先看添加一個(gè) Fragment 到布局 add() 的實(shí)現(xiàn):
@Override
public FragmentTransaction add(int containerViewId, Fragment fragment) {
doAddOp(containerViewId, fragment, null, OP_ADD);
return this;
......
}
可以看到添加一個(gè) Fragment 到布局很簡(jiǎn)單,概況一下就是:
修改 fragmentManager 和 ID,構(gòu)造成 Op,設(shè)置狀態(tài)信息,然后添加到列表里。
添加完了看看替換 replace 的實(shí)現(xiàn):
@Override
public FragmentTransaction replace(int containerViewId, Fragment fragment) {
return replace(containerViewId, fragment, null);
}
@Override
public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) {
if (containerViewId == 0) {
throw new IllegalArgumentException("Must use non-zero containerViewId");
}
doAddOp(containerViewId, fragment, tag, OP_REPLACE);
return this;
}
太可怕了,也是調(diào)用上面剛提到的 doAddOp() ,不同之處在于第四個(gè)參數(shù)為 OP_REPLACE ,看來(lái)之前小看了這個(gè)狀態(tài)值!
再看其他方法的實(shí)現(xiàn)就很簡(jiǎn)單了,無(wú)非就是構(gòu)造一個(gè) Op,設(shè)置對(duì)應(yīng)的狀態(tài)值。
@Override
public FragmentTransaction remove(Fragment fragment) {
Op op = new Op();
op.cmd = OP_REMOVE;
op.fragment = fragment;
addOp(op);
return this;
}
@Override
public FragmentTransaction hide(Fragment fragment) {
Op op = new Op();
op.cmd = OP_HIDE;
op.fragment = fragment;
addOp(op);
return this;
}
@Override
public FragmentTransaction show(Fragment fragment) {
Op op = new Op();
op.cmd = OP_SHOW;
op.fragment = fragment;
addOp(op);
return this;
}
那這些狀態(tài)值的不同是什么時(shí)候起作用的呢?
別忘了我們操作 Fragment 還有最后一步,提交。
看看這兩個(gè)是怎么實(shí)現(xiàn)的:
@Override
public int commit() {
return commitInternal(false);
}
@Override
public int commitAllowingStateLoss() {
return commitInternal(true);
}
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
//...
}
}
前面已經(jīng)介紹過(guò)了, FragmentManager.enqueueAction() 最終是使用 Handler 實(shí)現(xiàn)的異步執(zhí)行。
現(xiàn)在的問(wèn)題是執(zhí)行的任務(wù)是啥?
答案就是 Handler 發(fā)送的任務(wù) mExecCommit :
代碼多了一點(diǎn)省略掉了,但我們終于找到了最終的實(shí)現(xiàn):Handler 異步發(fā)到主線,調(diào)度執(zhí)行后,聚合、修改 Ops的狀態(tài),然后遍歷、修改 Fragment 棧中的 View 的狀態(tài)。
3.4.真正處理的部分
前面主要是對(duì) Fragment 的包裝類 Ops 進(jìn)行一些狀態(tài)修改,真正根據(jù) Ops 狀態(tài)進(jìn)行操作在這個(gè)部分:
/**
* Executes the operations contained within this transaction. The Fragment states will only
* be modified if optimizations are not allowed.
*/
void executeOps() {
final int numOps = mOps.size();
for (int opNum = 0; opNum < numOps; opNum++) {
final Op op = mOps.get(opNum);
final Fragment f = op.fragment;
f.setNextTransition(mTransition, mTransitionStyle); switch (op.cmd) {
case OP_ADD:
f.setNextAnim(op.enterAnim);
mManager.addFragment(f, false);
break;
case OP_REMOVE:
f.setNextAnim(op.exitAnim);
mManager.removeFragment(f);
break;
case OP_HIDE:
f.setNextAnim(op.exitAnim);
mManager.hideFragment(f);
break;
case OP_SHOW:
f.setNextAnim(op.enterAnim);
mManager.showFragment(f);
break;
case OP_DETACH:
f.setNextAnim(op.exitAnim);
mManager.detachFragment(f);
break;
case OP_ATTACH:
f.setNextAnim(op.enterAnim);
mManager.attachFragment(f);
break;
default:
throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
}
if (!mAllowOptimization && op.cmd != OP_ADD) {
mManager.moveFragmentToExpectedState(f);
}
}
if (!mAllowOptimization) {
// Added fragments are added at the end to comply with prior behavior.mManager.moveToState(mManager.mCurState, true);
}
}
FragmentManager 對(duì)這些方法的實(shí)現(xiàn)也很簡(jiǎn)單,修改 Fragment 的狀態(tài)值,比如remove(Fragment) :
public void removeFragment(Fragment fragment) {
if (DEBUG) Log.v(TAG, "remove: " + fragment + " nesting=" + fragment.mBackStackNesting);
final boolean inactive = !fragment.isInBackStack();
if (!fragment.mDetached || inactive) {
if (mAdded != null) {
mAdded.remove(fragment);
}
if (fragment.mHasMenu && fragment.mMenuVisible) {
mNeedMenuInvalidate = true;
}
fragment.mAdded = false; //設(shè)置屬性值
fragment.mRemoving = true;
}
}
代碼很長(zhǎng),先省略掉......但做的事情很簡(jiǎn)單:
根據(jù)狀態(tài)調(diào)用對(duì)應(yīng)的生命周期方法
如果是新創(chuàng)建的,就把布局添加到 ViewGroup 中
四丶總結(jié)
OK,看完這篇文章,相信對(duì)開(kāi)頭提出的問(wèn)題你已經(jīng)有了答案,這里再總結(jié)一下。
Fragment、FragmentManager、FragmentTransaction 關(guān)系
Fragment
其實(shí)是對(duì) View 的封裝,它持有 view, containerView, fragmentManager,
childFragmentManager 等信息
FragmentManager
是一個(gè)抽象類,它定義了對(duì)一個(gè) Activity/Fragment 中 添加進(jìn)來(lái)的 Fragment 列表、Fragment 回退棧的操作、管理方法
還定義了獲取事務(wù)對(duì)象的方法
具體實(shí)現(xiàn)在 FragmentImpl 中
FragmentTransaction
定義了對(duì) Fragment 添加、替換、隱藏等操作,還有四種提交方法
具體實(shí)現(xiàn)是在 BackStackRecord 中
Fragment 如何實(shí)現(xiàn)布局的添加替換
通過(guò)獲得當(dāng)前 Activity/Fragment 的FragmentManager/ChildFragmentManager,進(jìn)而拿到事務(wù)的實(shí)
現(xiàn)類 BackStackRecord,它將目標(biāo) Fragment 構(gòu)造成 Ops(包裝Fragment和狀態(tài)信息),然后提交給FragmentManager 處理。
如果是異步提交,就通過(guò) Handler 發(fā)送 Runnable 任務(wù),FragmentManager 拿到任務(wù)后,先處理 Ops
狀態(tài),然后調(diào)用 moveToState() 方法根據(jù)狀態(tài)調(diào)用 Fragment 對(duì)應(yīng)的生命周期方法,從而達(dá)到Fragment 的添加、布局的替換隱藏等。
下面這張圖從下往上看就是一個(gè) Fragment 創(chuàng)建經(jīng)歷的方法:
嵌套 Fragment的原理
也比較簡(jiǎn)單,Fragment 內(nèi)部有一個(gè) childFragmentManager,通過(guò)它管理子 Fragment。
在添加子 Fragment 時(shí),把子 Fragment 的布局 add 到父 Fragment 即可
由于很多代碼太長(zhǎng)了,敲了一下午,眼睛和手都酸了,對(duì)這樣感興趣的可以拿這份筆記自己研究,有不懂的歡迎留言
知識(shí)匯總的PDF相關(guān)內(nèi)容后續(xù)GitHub更新,想沖擊金三銀四的小伙伴可以找找看看,歡迎star
(順手留下GitHub鏈接,需要獲取相關(guān)面試等內(nèi)容的可以自己去找)
https://github.com/xiangjiana/Android-MS
總結(jié)
以上是生活随笔為你收集整理的android面试之fragment,当你面试的时候,被问到关于Fragment的种种的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: unity3d读取android文本文件
- 下一篇: android碎片实验报告,实验报告