关于Fragment和Activity对比中的一些理解

简介

自从Android 3.0中引入fragments 的概念,根据词海的翻译可以译为:碎片、片段。其目的是为了解决不同屏幕分辩率的动态和灵活UI设计。大屏幕如平板小屏幕如手机,平板电脑的设计使得其有更多的空间来放更多的UI组件,而多出来的空间存放UI使其会产生更多的交互,从而诞生了fragments 。

fragments 的设计不需要你来亲自管理view hierarchy 的复杂变化,通过将Activity 的布局分散到frament 中,可以在运行时修改activity 的外观,并且由activity 管理的back stack 中保存些变化。当一个片段指定了自身的布局时,它能和其他片段配置成不同的组合,在活动中为不同的屏幕尺寸修改布局配置(小屏幕可能每次显示一个片段,而大屏幕则可以显示两个或更多)。

Fragment必须被写成可重用的模块。因为fragment有自己的layout,自己进行事件响应,拥有自己的生命周期和行为,所以你可以在多个activity中包含同一个Fragment的不同实例。这对于让你的界面在不同的屏幕尺寸下都能给用户完美的体验尤其重要。

Fragment优点

  • Fragment可以使你能够将activity分离成多个可重用的组件,每个都有它自己的生命周期和UI。
  • Fragment可以轻松得创建动态灵活的UI设计,可以适应于不同的屏幕尺寸。从手机到平板电脑。
  • Fragment是一个独立的模块,紧紧地与activity绑定在一起。可以运行中动态地移除、加入、交换等。
  • Fragment提供一个新的方式让你在不同的安卓设备上统一你的UI。
  • Fragment 解决Activity间的切换不流畅,轻量切换。
  • Fragment 替代TabActivity做导航,性能更好。
  • Fragment 在4.2.版本中新增嵌套fragment使用方法,能够生成更好的界面效果。
  • Fragment做局部内容更新更方便,原来为了到达这一点要把多个布局放到一个activity里面,现在可以用多Fragment来代替,只有在需要的时候才加载Fragment,提高性能。
  • 可以从startActivityForResult中接收到返回结果,但是View不能。

Fragment和Activity生命周期对比

先让我们来看一下下面这一张Fragment和Activity的完整的生命周期的流程对比图

Fragment和Activity的完整的生命周期的流程对比图

从这张图来看生命周期对比非常的明确。因此在这个地方不在做非常详细的对比和解释。

下面重点来说一下getActivity()经常null的情况,在Fragment里边有attach和dettach方法。其实这样拿到的activity都不是为null的,而且Fragment的大部分生命周期中都是可以拿到宿主Activity的。那为何会出现空的情况呢?

现在对Fragment的管理类FragmentManager做一下全面的分析:

Android采用了FragmentManager对所有创建的Fragment进行了按照tag方式的缓存机制,每次生命周期的执行如果系统存在相同tag的Fragment时候,这个Fragment会被重新利用。但是如果使用者不知道这样的机制的话,那么获取到当前绑定的Activity很有可能是一个过期被回收的类。(这样的场景在内存吃紧的情况下是必定出现的。)

最正确的做法代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object instantiateItem(ViewGroup container, int position) {
if(this.mCurTransaction == null) {
this.mCurTransaction = this.mFragmentManager.beginTransaction();
}
String name = getItem(position).getClass().getCanonicalName();
Fragment fragment = this.mFragmentManager.findFragmentByTag(name);
if(fragment != null) {
this.mCurTransaction.attach(fragment);
} else {
fragment = this.getItem(position);
this.mCurTransaction.add(container.getId(), fragment, name);
}

if(fragment != this.mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}

return fragment;
}

这是笔者在做fragment+viewpager的时候遇到的问题,这段代码是针对adapter的重写。

Fragment页面相互切换的生命周期对比–场景演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
切换到该Fragment
11-29 14:26:35.095: D/AppListFragment(7649): onAttach
11-29 14:26:35.095: D/AppListFragment(7649): onCreate
11-29 14:26:35.095: D/AppListFragment(7649): onCreateView
11-29 14:26:35.100: D/AppListFragment(7649): onActivityCreated
11-29 14:26:35.120: D/AppListFragment(7649): onStart
11-29 14:26:35.120: D/AppListFragment(7649): onResume

屏幕灭掉:
11-29 14:27:35.185: D/AppListFragment(7649): onPause
11-29 14:27:35.205: D/AppListFragment(7649): onSaveInstanceState
11-29 14:27:35.205: D/AppListFragment(7649): onStop

屏幕解锁
11-29 14:33:13.240: D/AppListFragment(7649): onStart
11-29 14:33:13.275: D/AppListFragment(7649): onResume

切换到其他Fragment:
11-29 14:33:33.655: D/AppListFragment(7649): onPause
11-29 14:33:33.655: D/AppListFragment(7649): onStop
11-29 14:33:33.660: D/AppListFragment(7649): onDestroyView

切换回本身的Fragment:
11-29 14:33:55.820: D/AppListFragment(7649): onCreateView
11-29 14:33:55.825: D/AppListFragment(7649): onActivityCreated
11-29 14:33:55.825: D/AppListFragment(7649): onStart
11-29 14:33:55.825: D/AppListFragment(7649): onResume

回到桌面
11-29 14:34:26.590: D/AppListFragment(7649): onPause
11-29 14:34:26.880: D/AppListFragment(7649): onSaveInstanceState
11-29 14:34:26.880: D/AppListFragment(7649): onStop

回到应用
11-29 14:36:51.940: D/AppListFragment(7649): onStart
11-29 14:36:51.940: D/AppListFragment(7649): onResume

退出应用
11-29 14:37:03.020: D/AppListFragment(7649): onPause
11-29 14:37:03.155: D/AppListFragment(7649): onStop
11-29 14:37:03.155: D/AppListFragment(7649): onDestroyView
11-29 14:37:03.165: D/AppListFragment(7649): onDestroy
11-29 14:37:03.165: D/AppListFragment(7649): onDetach

比Activity多了一些生命周期,完整和Activity对接上

一个完善的app应该考虑好自己Fragment内部每个生命周期必须干的事情,比如onCreateView()和onViewCreated()都是不一样的。他们代表的view是否真的加载到当前app中。所以不同阶段Fragment的生命周期体现的状态时很不一样的。

Fragment和Activity、Fragment和Fragment之间通信

官方文档推荐使用回调的方式来进行Fragment和Activity、Fragment和Fragment之间通信。但是从笔者个人开发经验来讲我更倾向于使用EventBus

关于采用Fragment来逐步替代Activity的一些建议

1、使用Activity做一个容器,在每次启动新页面时候直接将启动的Fragment装入到Activity容器中,这样在Manifest中只需要注册这个Activity容器就ok了。(但是带来的问题:activity暂用了没有必要的内存)

2、采用一个MainActivity+FragmentManager的方式来对所有的Fragment进行一个集中管理。这种解决方案从性能和代码量来讲都是最好的。但是可能各种页面切换的时候逻辑维护要求比较高。

另外,本人贴一下关于本人在实际项目中的一种解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

/**
* 负责装载Fragment的容器
* @author Frodo
*/
public class LoaderActivity extends BaseActivity {

private FrameLayout rootView;
private Fragment rootFragment;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

rootView = new FrameLayout(this);
rootView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
rootView.setId(getRootViewID());
setContentView(rootView);

Uri uri = getIntent().getData();
if (uri == null) {
setError(400, null);
return;
}

String fragmentName = uri.getFragment();
if (fragmentName == null) {
setError(401, null);
return;
}

if (savedInstanceState != null) {
return;
}

try {
rootFragment = (Fragment) getClassLoader().loadClass(fragmentName).newInstance();
} catch (Exception e) {
setError(402, e);
Log.e("loader", "load fragment failed", e);
return;
}

FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(getRootViewID(), rootFragment);
// ft.add(getRootViewID(), rootFragment);
ft.commit();

}

/**
* 提供根布局的ID
*
* @return
*/
protected int getRootViewID() {
return android.R.id.primary;
}

public Fragment getRootFragment() {
return rootFragment;
}

/**
* 加载页面遇到错误时候的处理
*
* @param errorCode
* @param e
*/
protected void setError(int errorCode, Exception e) {
rootView.removeAllViews();
TextView text = new TextView(this);
text.setText("载入页面失败 (" + (errorCode > 0 ? errorCode : -1) + ")");
if (BuildConfig.DEBUG) {
if (e != null) {
text.append("\n");
text.append(e.toString());
}
}
text.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
rootView.addView(text);
}
}

那么在实际使用中通过如下方式完成页面的切换,所有页面采用Fragment的方式。在manifist文件中只需要注册很少的activity即可。

1
2
3
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("btm://login"));
intent.putExtra(LoginFragmentSale.SPLASH_LOGIN, true);
startActivity(intent);

当然通过schema方式,必须有一个注册地方。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MappingManager {

public MyMappingManager(Context ctx) {
super(ctx);
}

@Override
protected MappingSpec read() {
List<PageSpec> pages = new ArrayList<PageSpec>();

// ********* 新增scheme支持在此添加项 *********
// 依次为host、fragment(可选)、activity(可选)、是否需要login

// 登录页面
pages.add(new PageSpec("login", LoginFragment.class, null, false));

// other页面 。。。

MappingSpec mapping = new MappingSpec(BULoaderActivity.class, pages.toArray(new PageSpec[pages.size()]));
return mapping;
}
}

这里的read()方法是在Application中发起调用注册的。

发现还是有同学不清楚我们到底怎么做的。因此,下面再详细说一下这个过程:

首先,btm前缀会在manifest中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
<activity
android:name=".RedirectActivity"
android:label="redirect"
android:screenOrientation="portrait" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="btm" />
</intent-filter>
</activity>

这样就可以接收到所有btm开头的scheme了。再来看看RedirectActivity中具体做跳转的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected void doRedirect() {
Intent orig = getIntent();
Intent intent = new Intent(orig.getAction(), orig.getData());
intent.putExtras(orig);
intent = urlMap(intent);
try {
// 避免进入死循环
List<ResolveInfo> l = getPackageManager().queryIntentActivities(
intent, 0);
if (l.size() == 1) {
ResolveInfo ri = l.get(0);
if (getPackageName().equals(ri.activityInfo.packageName)) {
if (getClass().getName().equals(ri.activityInfo.name)) {
throw new Exception("infinite loop");
}
}
} else if (l.size() > 1) {
// should not happen, do we allow this?
}
startActivity(intent);
finish();
} catch (Exception e) {
setError(402, e);
Log.e("app", "unable to redirect " + getIntent(), e);
}
}

urlMap是非常重要的scheme分发映射点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public Intent urlMap(Intent intent) {
do {
Uri uri = intent.getData();
if (uri == null) {
break;
}
if (uri.getScheme() == null
|| !PRIMARY_SCHEME.equals(uri.getScheme())) {
break;
}

MappingManager mManager = mappingManager();
if (mManager == null) {
break;
}

MappingSpec mSpec = mManager.mappingSpec();
if (mSpec == null) {
break;
}

String host = uri.getHost();
if (TextUtils.isEmpty(host))
break;
host = host.toLowerCase();

PageSpec page = mSpec.getPage(host);
if (page == null) {
Log.w("loader", "host (" + host
+ ") Can't find the page in mapping.");
break;
}
Class<?> fragment = page.fragment;

intent.putExtra("_login", page.login);

Class<?> defaultLoader = mSpec.loader;
Class<?> loader = null;
if (page.activity != null) {
loader = page.activity;

} else if (defaultLoader != null) {// defaultLoader is always null
loader = defaultLoader;
}

if (loader != null) {
intent.setClass(this, loader);

} else {
intent.setClass(this, LoaderActivity.class);
}

String query = uri.getQuery();

uri = Uri.parse(String.format("%s://%s?%s#%s", uri.getScheme(),
host, query, fragment == null ? "" : fragment.getName()));
intent.setData(uri);

} while (false);

return intent;
}

在这里会有一个很关键的地方就是intent.setClass(this, LoaderActivity.class),通过这样可以加载Fragment承载的Activity。
另外这样做大伙儿可能觉得比较繁琐,但是这个通过Uri的方式来做的意义就是比如当你的应用需要像系统打电话,发短信这种功能的时候。这将是非常好的实现方式。

OK,这篇文章连续补了几次。该写的应该都写出来了,也感谢各位的关注。

Have fun!!!