ViewPager + FragmentPagerAdapter,時我們經常使用的一對搭檔,其實際應用的代碼也非常簡單,但是也有一些容易被忽略的地方,這次我們就來討論下FragmentPagerAdapter對Fragment的緩存應用。
我們可以先看看最簡單的實現,自定義Adapter如下:
[代碼]java代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public
class
CustomPagerAdapter
extends
FragmentPagerAdapter{
private
List<fragment> mFragments;
public
CustomPagerAdapter(FragmentManager fm, List<fragment> fragments) {
super
(fm);
this
.mFragments = fragments;
fm.beginTransaction().commitAllowingStateLoss();
}
@Override
public
Fragment getItem(
int
position) {
return
this
.mFragments.get(position);
}
@Override
public
int
getCount() {
return
this
.mFragments.size();
}
@Override
public
long
getItemId(
int
position) {
return
position;
}
}</fragment></fragment>
|
代碼比較簡單,就不解釋了,接着在Activity中使用這個Adapter:
[代碼]java代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
ViewPager pager = (ViewPager) findViewById(R.id.view_pager);
List<fragment> fragmentList =
new
ArrayList<>()
TestFragment fragmentOne =
new
TestFragment();
fragmentOne.setText(
"One"
);
TestFragment fragmentTwo =
new
TestFragment();
fragmentTwo.setText(
"Two"
);
TestFragment fragmentThree =
new
TestFragment();
fragmentThree.setText(
"Three"
);
fragmentList.add(fragmentOne);
fragmentList.add(fragmentTwo);
fragmentList.add(fragmentThree);
CustomPagerAdapter adapter =
new
CustomPagerAdapter(getSupportFragmentManager(), fragmentList);
pager.setAdapter(adapter);</fragment>
|
這樣就完成了一個FragmentPagerAdapter最基本的應用。現在,看上去一切都如我們所願,但是真的沒有任何問題了嗎?
接下來,我們來模擬一下程序運行在后台時,Android系統由於內存緊張,殺掉我們程序進程的情況:
- 首先運行程序至前台
- 接下來,點擊Home鍵,返回桌面,同時我們的程序退回至后台運行。
- 進入Android Studio中,點擊Android Monitor這個tab,並選擇當前Device,並選擇我們程序的進程名。
- 點擊Terminal Application這個小紅叉按鈕,如下圖:
- 這個時候后台進程已經被殺掉了,但是應用程序歷史里我們的應用還在,所以長按Home鍵,並選擇我們的程序,讓其恢復到前台。
- 這時會看到,程序的確恢復到之前的頁面。但奇怪的是,頁面上卻只有Hello,我們之前傳入的Two到哪里去了?
Fragment代碼也比較簡單,通過日志,我們發現恢復時,mText字段為空。所以頁面上對應的TextView無法顯示。
[代碼]java代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
public
class
TestFragment
extends
Fragment {
private
String mText;
@Nullable
@Override
public
View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_test, container,
false
);
TextView textView = (TextView) view.findViewById(R.id.center_text_view);
textView.setText(mText);
return
view;
}
public
void
setText(String text) {
this
.mText = text;
}
}
|
我們知道,以上是模擬Android系統內存緊張時,殺掉后台應用的流程。另外,當用戶安裝了類似360安全管家等應用,選擇清理內存時,也會觸發以上情況。
那么當上面的流程發生時,Activity的onSaveInstanceState會被調用,以便我們可以保存當前的用戶數據/頁面狀態等。當恢復時,在onCreate時,我們通過savedInstanceState參數,可以取到之前存儲的數據,然后重新綁定到View上。
這個過程都可以理解,可是回到我們的Activity代碼當中:
[代碼]java代碼:
01
02
03
04
05
06
07
08
09
10
11
12
|
TestFragment fragmentOne =
new
TestFragment();
fragmentOne.setText(
"One"
);
TestFragment fragmentTwo =
new
TestFragment();
fragmentTwo.setText(
"Two"
);
TestFragment fragmentThree =
new
TestFragment();
fragmentThree.setText(
"Three"
);
fragmentList.add(fragmentOne);
fragmentList.add(fragmentTwo);
fragmentList.add(fragmentThree);
CustomPagerAdapter adapter =
new
CustomPagerAdapter(getSupportFragmentManager(), fragmentList);
pager.setAdapter(adapter);
|
這段代碼,是在onCreate方法中調用的。應用從后台恢復的時候,這段代碼是被完整的執行過的。既然這樣,三個Fragment都被重新創建過,並設置過對應的Text值,那么為什么Fragment中mText字段仍然為空呢?
難道說,呈現在屏幕上的Fragment,和我們在onCreate中實例化的Fragment,已然不是同一個實例?
為了驗證這個想法,在OnCreate中加入下面的日志:
[代碼]java代碼:
1
2
3
|
TestFragment fragmentOne =
new
TestFragment();
fragmentOne.setText(
"One"
);
Log.i(
"test"
,
"++++fragmentOne++++:"
+ fragmentOne.toString());
|
同時在TestFragment的onCreateView方法中也記下日志:
[代碼]java代碼:
1
|
Log.i(
"test"
,
"++++current fragment++++:"
+
this
.toString());
|
第一次運行,恩,沒有問題。創建和運行的都是同一個實例 534ed278
[代碼]java代碼:
1
2
|
I/test: ++++fragmentOne++++:TestFragment{534ed278}
I/test: ++++current fragment++++:TestFragment{534ed278 #
0
id=
0x7f0c0066
android:switcher:
2131492966
:
0
}
|
接下來,我們再次進行殺進程並恢復的過程。日志輸出為:
[代碼]java代碼:
1
2
|
I/test: ++++fragmentOne++++:TestFragment{534c5c30}
I/test: ++++current fragment++++:TestFragment{534d10d4 #
0
id=
0x7f0c0066
android:switcher:
2131492966
:
0
}
|
額。。果然,這次我們創建的Fragment,和實際經過onCreateView的Fragment。並不是同一個(534c5c30/534d10d4)。
看來,還是要從源碼中尋求真相,打開FragmentPagerAdapter的源碼,在instantiateItem方法中發現了下面這一段:
[代碼]java代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if
(fragment !=
null
) {
if
(DEBUG) Log.v(TAG,
"Attaching item #"
+ itemId +
": f="
+ fragment);
mCurTransaction.attach(fragment);
}
else
{
fragment = getItem(position);
if
(DEBUG) Log.v(TAG,
"Adding item #"
+ itemId +
": f="
+ fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
|
makeFragmentName方法如下:
[代碼]java代碼:
1
2
3
|
private
static
String makeFragmentName(
int
viewId,
long
id) {
return
"android:switcher:"
+ viewId +
":"
+ id;
}
|
原來,在實例化Fragment的時候,FragmentPagerAdapter會先通過makeFragmentName返回的tag,到FragmentManager當中進行查找是否有當前Fragment的緩存,如果有的話,就直接將之前的Fragment恢復回來並使用;反之,才會使用我們傳入的新實例。
而makeFragmentName產生的tag,只受我們重寫的getItemId()方法返回值,和當前容器View的Id,container.getId()的影響。
到這里,問題就清楚了,由於FragmentPagerAdapter會主動的去取緩存當中的Fragment,所以導致恢復回來之后,Fragment的實例不一樣的問題。
至於為什么mText字段為空,以及怎樣解決這個情況,我們下一篇再來討論^_^。