Android ViewPager 与 Fragment 懒加载

ViewPager + 多 Fragment 的模式很常用,但是 ViewPager 存在预加载的问题,如果多个 Fragment 都存在大量的网络请求或读写情况,就影响了 APP 性能和体验。在网上找到了一个比较好的解决方法,方法就是保留 ViewPager 的预加载,在 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
/**
* Created by wuzy on 2018/4/23.
**/
public abstract class BasePagerFragment extends BaseFragment {

private static final String TAG = "BasePagerFragment";

protected boolean isViewInitiated;
protected boolean isVisibleToUser;
protected boolean isDataInitiated;

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

@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
isViewInitiated = true;
prepareLoadData();
Log.e(TAG, "onActivityCreated: ");
}

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
this.isVisibleToUser = isVisibleToUser;
prepareLoadData();
Log.e(TAG, "setUserVisibleHint: " );
}

public abstract void loadData();

public boolean prepareLoadData() {
return prepareLoadData(false);
}

public boolean prepareLoadData(boolean forceUpdate) {
if (isVisibleToUser && isViewInitiated && (!isDataInitiated || forceUpdate)) {
loadData();
isDataInitiated = true;
return true;
}
return false;
}
}

这里简单解释下,setUserVisibleHint 方法可被用来判断 Fragment UI 是否可见。注意的是,如果项目中没有使用 ViewPager 的话,这个方法压根不调用。
这里,在 UI 可见的时候的时候 调用 prepareLoadData 方法,在 prepareLoadData 方法中做判断:如果 UI 可见,并且 Fragment 已经初始化完毕,数据未加载或者需要强制加载数据的情况下,进行数据加载。

在某些情况下可能想禁掉 ViewPager 的滑动,可以自定义 ViewPager,提供设置滑动的方法。

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
/**
* Created by wuzy on 2018/4/23.
**/
public class CustomViewPager extends ViewPager {

private boolean isPageChangeEnabled = true;

public CustomViewPager(Context context) {
super(context);
}

public CustomViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
return this.isPageChangeEnabled && super.onTouchEvent(event);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return this.isPageChangeEnabled && super.onInterceptTouchEvent(event);
}

/**
* 设置是否可以滑动
* @param b
*/
public void setPageChangeEnabled(boolean b) {
this.isPageChangeEnabled = b;
}
}

具体 Demo 地址 :ViewPagerDemo

推荐一个好用的 Android 屏幕适配的插件

Android 屏幕适配一直是个消耗时间,但没啥意思的活。

关于 Android 屏幕适配的基本知识可以参考这篇文章:

今天推荐一种适配方案,能够节约大部分适配时间。

dp 适配

我们知道获取屏幕最小宽度 dp 值的方法:

1
2
3
4
5
6
7
8
9
DisplayMetrics dm = new DisplayMetrics();

getWindowManager().getDefaultDisplay().getMetrics(dm);

int widthPixels = dm.widthPixels;

float density = dm.density;

float widthDP = widthPixels / density;

而 dp 适配的原理就是根据 dp 值等比缩放。根据“最小宽度(Smallest-width)限定符”,即如果当前设备最小宽度(以 dp 为单位)为 400dp,那么系统会自动找到对应的 values-sw400dp 文件夹下的 dimens.xml 文件。

所以如果可以以某一 widthDP 为基准,生成所有设备对应的 dimens.xml 文件,就可以省去很多适配时间了。

网上已经有大神写了一个自动生成的插件,非常好用。下面介绍下使用方法。

1、首先是下载 ScreenMatch 插件,这个不用多讲了,直接在 Android Studio 中下载即可,安装完成重启 AS。

2、在项目的默认 values 文件夹中需要提供一份 dimens.xml 文件。这个文件中的 dp 值和 sp 值就是基准值。写法如下。

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
<?xml version="1.0" encoding="UTF-8"?>
<resources>

<!-- Your custom size defind by references, can be writted in anywhere, any module, any values/*.xml, for example: -->
<dimen name="card_common_margin_left">@dimen/dp_15</dimen>

<!-- dp and sp values, must be defind in this file! -->
<!-- view size,you can add if there is no one -->
<dimen name="dp_m_60">-60dp</dimen>
<dimen name="dp_m_30">-30dp</dimen>
<dimen name="dp_m_20">-20dp</dimen>
<dimen name="dp_m_12">-12dp</dimen>
<dimen name="dp_m_10">-10dp</dimen>
<dimen name="dp_m_8">-8dp</dimen>
<dimen name="dp_m_5">-5dp</dimen>
<dimen name="dp_m_2">-2dp</dimen>
<dimen name="dp_m_1">-1dp</dimen>
<dimen name="dp_0">0dp</dimen>
<dimen name="dp_0_1">0.1dp</dimen>
<dimen name="dp_0_5">0.5dp</dimen>
<dimen name="dp_1">1dp</dimen>
<dimen name="dp_1_5">1.5dp</dimen>
<dimen name="dp_2">2dp</dimen>

<!-- font size,you can add if there is no one -->
<dimen name="sp_6">6sp</dimen>
<dimen name="sp_7">7sp</dimen>

</resources>

3、在任何目录下右键选择 ScreenMatch 选项,执行自动生成其他 widthDP 设备对应的 dimens.xml 文件。

4、插件默认的 widthDP 基准值是 360dp,一般手机的 widthDP 都是 360dp。这个插件会自动生成 widthDP 为 384,392,400,410,411,480,533,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365 下的 dimen 文件。

5、当然你也可以通过修改以下配置文件的属性来满足自己的需求。记得修改配置后,需要删除自动生成的文件夹,重新点击 ScreenMatch 生成。

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
############################################################################
# Start with '#' is annotate. #
# In front of '=' is key, cannot be modified. #
# More information to visit: #
# http://blog.csdn.net/fesdgasdgasdg/article/details/52325590 #
# http://download.csdn.net/detail/fesdgasdgasdg/9913744 #
# https://github.com/mengzhinan/PhoneScreenMatch #
############################################################################
#
# You need to refresh or reopen the project every time you modify the configuration,
# or you can't get the latest configuration parameters.
#
#############################################################################
#
# Base dp value for screen match. Cut the screen into [base_dp] parts.
# Data type is double. System default value is 360.
# I advise you not to modify the value, be careful !!!!!!!!! _^_ *_*
base_dp=360 // widthDP 默认基准
# Also need to match the phone screen of [match_dp].
# If you have another dp values.
# System default values is 384,392,400,410,411,480,533,592,600,640,662,720,768,800,811,820,960,961,1024,1280,1365
match_dp= // 可以添加你想要的 widthDP 值
# If you not wanna to match dp values above. Write some above values here, append value with "," .
# For example: 811,961,1365
ignore_dp= // 添加不需要生成的 widthDP 值
# They're not android module name. If has more,split with , Symbol.
# If you set, it will not show in SelectDialog.
# If you have, write here and append value with "," .
# For example: testLibrary,commonModule
# System default values is .gradle, gradle, .idea, build, .git
ignore_module_name=
# Use which module under the values/dimen.xml file to do the base file,
# and generated dimen.xml file store in this module?
# Default value is 'app'.
match_module=app
# Don't show select dialog again when use this plugin.
# System screen match will use the last selected module name or default module name.
# You can give value true or false. Default value is false.
not_show_dialog=false
# Do you want to generate the default example dimens.xml file?
# In path of .../projectName/screenMatch_example_dimens.xml, It does not affect your project code.
# You can give value true or false. Default value is false.
not_create_default_dimens=false
# Does the font scale the same size as the DP? May not be accuracy.
# You can give value true or false. Default value is true. Also need scaled.
is_match_font_sp=true
# Do you want to create values-wXXXdp folder or values-swXXXdp folder ?
# I suggest you create values-swXXXdp folder,
# because I had a problem when I was working on the horizontal screen adapter.
# values-swXXXdp folder can solve my problem.
# If you want create values-swXXXdp folder, set "create_values_sw_folder=true",
# otherwise set "create_values_sw_folder=true".
# Default values is true.
create_values_sw_folder=true

6、对于其他 module, 只需要在 values 文件夹下有一套与 app module 一样的 dimens 文件即可达到适配。

具体的插件原理以及更多细节可以直接去看插件作者的文章 Android dp方式的屏幕适配工具使用(Android Studio插件方式),非常感谢作者。

Android 绘制虚线

Android 中绘制虚线可采用添加在布局文件中添加一个 View,然后给这个 View 加个虚线背景即可。

一般都是用 shape 绘制虚线 :

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line">

<stroke
android:dashGap="2dp"
android:dashWidth="2dp"
android:color="@color/colorAccent"
android:width="1dp"/>

</shape>

然后在需要绘制虚线的地方加个 View :

1
2
3
4
<View
android:layout_width="wrap_content"
android:layout_height="2dp"
android:background="@drawable/shape_dashed"/>

很简单。

但运行到真机上发现并不是虚线,而是一条实线。网上一堆博客文章都是这种做法,不知道他们都验证过没有。。。

查了一些资料,才知道绘制的虚线变成实线的原因是 dashGap 不支持硬件加速,而我们的手机默认是开启了硬件加速的。因此只需要修改下 View 的属性即可。

1
2
3
4
5
<View
android:layout_width="wrap_content"
android:layout_height="2dp"
android:background="@drawable/shape_dashed"
android:layerType="software"/>

此外要注意 View 的高度要比 shape 中定义的高度要大,不然不显示。都是一些细节,虽然没什么技术含量,但最近发现很多 Bug 都是低级错误导致的,都是平时不注重细节的原因。在此记录一下,提醒自己。

保险概览

了解了些保险知识,整理了部分导图笔记。

insurance

Android setContentView,Window,PhoneWindow and DecorView

从 Activity 的 setContentView(R.layout.activity_main) 开始。

1
2
3
4
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

这里调用了 getWindow().setContentView 方法,那这个 getWindow() 是什么呢?

1
2
3
4
5
6
7
8
9
10
11
/**
* Retrieve the current {@link android.view.Window} for the activity.
* This can be used to directly access parts of the Window API that
* are not available through Activity/Screen.
*
* @return Window The current window, or null if the activity is not
* visual.
*/
public Window getWindow() {
return mWindow;
}

从上面的 getWindow() 方法可以看到,获取的是当前 activity 的 Window 对象,如果 activity 不可见,返回 null。而 Window 是一个抽象类,所以这里的 mWindow.setContentView 方法调用的是 Window 实现类的方法。那 Window 的实现类是什么呢?

在 Activity 类的 attach 方法中,可以看出,PhoneWindow 即是 Window 的实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);

mFragments.attachHost(null /*parent*/);
// 创建 PhoneWindow 对象
mWindow = new PhoneWindow(this, window, activityConfigCallback);
... // 省略
}

所以我们直接去看 PhoneWindow 的 setContentView 方法。

1
2
3
4
5
6
7
8
9
10
11
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
... // 省略
}

如果 mContentParent == null,也就是当前内容未加载到窗口中,第一次加载时,调用 installDecor() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
... // 省略
}
}

installDecor() 方法中先是通过 generateDecor() 方法创建 DecorView 对象,DecorView 继承于 FrameLayout,用来作为整个PhoneWindow的根视图,但此时还只是个空架子。

1
2
3
4
protected DecorView generateDecor(int featureId) {
... // 省略
return new DecorView(context, featureId, this, getAttributes());
}

创建完成后,根据用户的设置,通过 generateLayout() 方法根据 Theme 从系统中选择创建默认布局。

引用 事件分发机制原理 中的例子阐述 Window、PhoneWindow 及 DecorView 之间的关系

简单来说,Window是一个抽象类,是所有视图的最顶层容器,视图的外观和行为都归他管,不论是背景显示,标题栏还是事件处理都是他管理的范畴,它其实就像是View界的太上皇(虽然能管的事情看似很多,但是没实权,因为抽象类不能直接使用)。

而 PhoneWindow 作为 Window 的唯一亲儿子(唯一实现类),自然就是 View 界的皇帝了,PhoneWindow 的权利可是非常大大,不过对于我们来说用处并不大,因为皇帝平时都是躲在深宫里面的,虽然偶尔用特殊方法能见上一面,但想要完全指挥 PhoneWindow 为你工作是很困难的。

而上面说的 DecorView 是 PhoneWindow 的一个内部类,其职位相当于小太监,就是跟在 PhoneWindow 身边专业为 PhoneWindow 服务的,除了自己要干活之外,也负责消息的传递,PhoneWindow 的指示通过 DecorView 传递给下面的 View,而下面 View 的信息也通过 DecorView 回传给 PhoneWindow。

《禅与摩托车维修艺术》摘录

1.

我们夫妻俩和一些老友迷上这种乡间小路已经有好些年了。当初为了调剂一下或是为了去另一条干道而走捷径,都不免要骑上一段。每次我们都会惊讶于景色的美丽,骑回原路时便有一种轻松愉悦的感觉。我们经常这么骑,后来才明白道理其实很简单:这些乡间小路和一般的干道迥然不同,就连沿线居住的居民的生活步调和个性也不一样。他们一直都没有离开过本地,所以可以很悠闲地和你寒暄问候、谈天说地,那感觉好极了。反而是那些早就搬到城市里的人和他们的子子孙孙迷失了,忘记了这种情怀。这实在是一个宝贵的发现。我在想,为什么我们这么久之后才会对其着迷。我们早已看过却仿佛没有看到,或者说是环境使我们视而不见,蒙骗了我们,让我们以为真正的生活是在大都市里,而这里只不过是落后的穷乡僻壤。这的确是件令人迷惘的事,就好像真理已经在敲你的门,而你却说:“走开,我正在寻找真理。”所以真理掉头就走了。哎,这种现象真是让人不解。

2.

当你做某件事的时候,一旦想要求快,就表示你再也不关心它,而想去做别的事。

3.

其实现代人未必比以前的人聪明,人的智商并没有多大改变,那些印第安人和中古世纪的人跟我们都差不多,但是彼此所处的环境不同;在以前的环境中,他们认为鬼神是存在的,就像现代人认为原子、质子、光子和量子是存在的。从这个角度来说,我相信有鬼,也就是说,现代人也有属于他们的鬼神,你知道的。”

4.

地心引力也没有自己的质量,没有自己的能量,当时人尚未出现,所以也不存在于人的心灵之中。它也不在空间里,因为也没有空间存在,更不存在于任何地方–这个地心引力仍然存在吗?”

5.

因果逻辑是思想上的产物,我认为精神疾病先于人的思想。

6.

人在思考和感觉的时候往往会偏向于某一种形式,而且会误解和看轻另一种形式。然而没有人会放弃自己所看到的真理,就我所知,目前还没有人可以真正融合两者,因为这两者之间根本就找不到交会点。

7.

熟悉往往也会使一个人视而不见。

8.

钢铁?钢铁也是人所设计出来的,因为在自然界之中并没有钢铁的存在,在远古的铜器时代,就有人能告诉你这个。自然界所有的,只是可以做钢铁的原料。但是什么又是原料呢?同样的这也是人所想出来的……鬼魂!

9.

把问题正确地写下来,起码要兼顾到六方面:
(1)问题是什么。
(2)假设问题的原因。
(3)证实每个问题的假设。
(4)预测实验的结果。
(5)观察实验的结果。
(6)由实验得出结论。

10.

运用科学方法的目的,就是要从许多假设当中找出正确的一个,这就是科学的目的。然而我们从科学的历史来看,事实恰恰相反。各种资料、史料、理论和假设不断大量地增加,科学把人从唯一绝对的真理,引向多元、摇摆不定、相对的世界,是造成社会混乱、思想价值混淆的主要元凶。而这一切现象原本是科学要消灭的。

11.

我们目前所谓的理性模式并没有把社会带向更美好的世界,反而离它愈来愈远。自从文艺复兴以来,这些模式就一直存在。只要人们主要的需求还在于衣食住行,这些模式就会存在下去,而且还会继续运作。但是对现在大部分的人来说,这些基本的需要不再是主要的问题,因而从古代流传下来的理性结构已经不符合所需,从而显露出它真正的面目–在情感上是空虚的,在美学上没有任何表现,而在灵性上更是一片空白。这就是它的现状,而且它还会持续很长的一段时间。

12.

如果你对事情有完全的信心,就不太可能产生狂热的态度。就拿太阳来说 吧,没有人会为了它明天会升起而兴奋不已,因为这是必然的现象。如果有人对政治或是宗教狂热,那是因为他对这些目标或是教义没有完全的信心。

13.

克伦威尔曾经说:”一个没有目标的人才能爬到最高。“

14.

任何想要以己为荣的目标,结局都非常悲惨。

15.

哲学上的神秘主义认为真理是无法界定的,自有历史以来就存在,只能通过非理性的方式了解。这就是禅的根基,但是这并不属于学校研究的范围。而学校这座理性教会主要就是研究那些能被界定的事物,所以一个人如果想研究神秘的主义,他就应该去修道院而不是去大学,大学要研究的是能够形之于文字的事物。

16.

从一开始唯心、唯物就没有很清楚的分野。

17.

过去”只存在于我们的记忆之中,”未来”则存在于我们的计划之中,而只有”现在”才是惟一的真实。你理智上所意识到的那棵树,由于这一小段的时间的关系,便属于过去,因而对你来说并不真实。任何经由思想所意识到的总是存在于过去,因而都不真实。所以真实总是存在于你所看到的那一刹那,且在你还没有意识到之前。除此之外,没有别的真实。这种在意识之前的真实,就是斐德洛所谓的良质。由于所有经由思想所认知的事物必须来自于这一段思考前的真实,所以良质是因,而果才是所有的主体以及客体。

18.

古典和浪漫必须从根本上融合在一起。

19.

佛教的禅宗提倡打坐,就是要使人物我两忘。而在我所提到的摩托车维修问题上,你只要专注地修理车子,就不会出现物我对立的情况。一旦真正地投入了工作之中,就可以说是在关心自己的工作,这就是关心的真正意义–对自己手中的工作产生认同感。当一个人产生这种认同感的时候,他就会看到关心的另外一面–良质。

20.

太阳已经下山了,我有一种强烈的寂寞感和孤独感,我的精神也随太阳西 沉了。

21.

如果你一时无法谦虚下来,有一个方法,就是无论如何也要装出这种态度。
因为如果你刻意地假设自己表现得不够好,那么一旦事实证明的确如此,你的进取心反而会提升。你会继续这样做,一直到事实证明你的假设是错误的。

Android Handler 消息处理机制

日常开发中,一般不会在子线程中直接进行 UI 操作,大部分采取的办法是创建 Message 对象,然后借助 Handler 发送出去,再在 Handler 的 handlerMessage() 方法中获取 Message 对象,进行一系列的 UI 操作。Handler 负责发送 Message, 又负责处理 Message, 其中经历了什么 ,需要从源码中一探究竟。

首先看 Handler 的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}

mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}

Handler 的无参构造函数会调用上面的重载构造函数,我们直接看第二个 if 语句, 如果 mLooper 为 null,将会抛出异常 “Can’t create handler inside thread that has not called Looper.prepare()”,意思是说如果没有调用 Looper.prepare(), 在当前线程中不能创建 Handler 实例。但是,我们在 UI 主线程中创建 Handler 时,好像并不要调用方法 Looper.prepare(),肯定是系统已经帮我们自动调用了 Looper.prepare() 方法。

我们来看 Looper.myLooper() 方法,这个方法直接从 sThreadLocal 取出 Looper 对象。

1
2
3
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}

所以 sThreadLocal 中的 Looper 对象肯定是在 Looper.prepare() 方法中 set 进去,Looper.prepare() 方法会调用 Looper.prepare(boolean quitAllowed) 方法。

1
2
3
4
5
6
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

可以看到,这个方法中首先判断当前线程中有没有 Looper 对象了,如果没有,就会创建一个 Looper 对象 set 到 sThreadLocal 中 ;如果有 Looper 对象了,就不能再创建 Looper 对象,即一个线程只能创建一个 Looper 对象。而在 UI 主线程中,系统是什么时候调用了 Looper.prepare() 方法呢?

查看 ActivityThread 中的 main() 方法,代码如下所示:

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
public static void main(String[] args) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
SamplingProfilerIntegration.start();

// CloseGuard defaults to true and can be quite spammy. We
// disable it here, but selectively enable it later (via
// StrictMode) on debug builds, but using DropBox, not logs.
CloseGuard.setEnabled(false);

Environment.initForCurrentUser();

// Set the reporter for event logging in libcore
EventLogger.setReporter(new EventLoggingReporter());

// Make sure TrustedCertificateStore looks in the right place for CA certificates
final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
TrustedCertificateStore.setDefaultUserDirectory(configDir);

Process.setArgV0("<pre-initialized>");

Looper.prepareMainLooper(); // 创建 Looper 对象

ActivityThread thread = new ActivityThread();
thread.attach(false);

if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}

if (false) {
Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread"));
}

// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");
}

查看 Looper.prepareMainLooper(); 这一行,应用启动的时候就调用了这个方法,在这个方法中又去调用了方法 prepare(boolean quitAllowed) 去创建一个 Looper 对象。

1
2
3
4
5
6
7
8
9
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}

至此,Handler 的创建过程已经清晰。再来看看 Looper 的构造函数如下:

1
2
3
4
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

可以看出在当前线程中创建了一个 MessageQueue,顾名思义,MessageQueue 是用来存放消息的消息队列。同样从这可以看出,一个线程只能创建一个 Looper,也就只有一个 MessageQueue。

那么,Handler 是如何发送消息并处理消息的呢?得从发送消息的方法看起,Handler 提供很多发送消息的方法,但大部分方法最终都会调用 sendMessageAtTime 方法。

1
2
3
4
5
6
7
8
9
10
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}

这个方法的第一个参数是发送的消息,第二个参数是自系统开机到当前时间的毫秒数再加上延迟时间,如果调用的不是 sendMessageDelayed() 方法,延迟时间就为 0。最后调用 enqueueMessage 方法。

1
2
3
4
5
6
7
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

这个方法中,首先将 msg.target 设置为 handler 对象本身,然后调用 MessageQueue 的 enqueueMessage 方法,我们再看 MessageQueue 的 enqueueMessage 方法。

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
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}

msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}

// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}

这个方法中主要就是将消息按照时间的先后顺序进行排队,通过 msg.next 指定下一个消息。

那么消息是如何出队的呢?这得看 Looper.loop() 源码了。

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
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}

// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}

final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

final long traceTag = me.mTraceTag;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
final long end;
try {
msg.target.dispatchMessage(msg); // 回调 Handler 的 dispatchMessage 方法
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (slowDispatchThresholdMs > 0) {
final long time = end - start;
if (time > slowDispatchThresholdMs) {
Slog.w(TAG, "Dispatch took " + time + "ms on "
+ Thread.currentThread().getName() + ", h=" +
msg.target + " cb=" + msg.callback + " msg=" + msg.what);
}
}

if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}

msg.recycleUnchecked();
}
}

主线程中,这个方法在 ActivityThread 中的 main() 方法中被调用。可以看出,在这个方法中进入了一个死循环,不断调用 MessageQueue.next() 方法从消息队列中获取出队消息,将消息传递到 msg.target 的 dispatchMessage() 方法中,这里的 msg.target 其实就是 Handler,是在 Handler 的 enqueueMessage 方法中设置的。然后看一看 Handler 中 dispatchMessage() 方法的源码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}

这里三种不同的调用方法都是对消息进行处理,提供不同的应用场景。

到这里 Android Handler 的基本原理一目了然了,对于其他一些在子线程中使用的方法,比如 Handler 的 post()方法 、Activity 的 runOnUiThread() 等方法,其背后的原理都是一样的,不再赘述。

总结

  • 基本概念
    • Message:消息。
    • MessageQueue:消息队列,用来存放 Handler 发送过来的 Message,并且按照时间顺序将消息排成队列。
    • Handler:消息处理者,负责发送和处理消息。
    • Looper:消息轮询器,不断的从 MessageQqueue 中取出 Message 交给 Handler 处理。
  • 应用启动时系统默认给 UI 主线程创建了 Looper 对象。如果是在子线程中创建 Handler 对象,需要先创建 Looper.prepare() 方法创建 Looper 对象。
  • 一个线程只能拥有一个 Looper 对象和 一个 MessageQueue 对象。

Android 多进程基本知识整理

在 Android 中,一个应用默认有一个进程。但我们可以通过配置实现在一个应用中开启多个进程。

开启多进程方式

  • 在清单文件中指定 android:process 属性
  • 适用元素:Activity,Service,Receiver,ContentProvider。
  • : 开头,表示这个进程是应用的私有进程;以小写字母开头,表示这个进程是全局进程,可以被多个应用公用。
1
2
3
4
5
6
<activity
android:name=".SecondActivity"
android:process=":second" />
<activity
android:name=".ThirdActivity"
android:process="com.example.wuzy.helloworld.third"/>

多进程的优点

系统为每个应用分配一定大小的内存,从之前的 16M 到 32M、48M,甚至更高。但毕竟有限。

进程是资源分配的基本单位。也就是说,一个应用有对个进程,那这个应用可以获得更多的内存。

所以,开启多进程可以分担主进程的内存消耗,常见音乐类 APP 的后台播放,应用的推送服务等。

多进程的不足

1、数据共享问题

Android 系统为每个进程分配独立的虚拟机,不同的虚拟机之间数据不能共享,即使是静态成员还是单例模式。

2、线程同步机制失效

不同进程锁的不是同一个对象,无法保证线程同步了。

3、SharedPreferences 可靠性下降

SharedPreferences 还没有增加对多进程的支持。

4、Application 多次创建

当一个组件跑在新的进程中,系统要在创建进程的同时为其分配独立的虚拟机,自然就会创建新的 Application。这就导致了 application 的 onCreate方法重复执行全部的初始化代码。因此,可以根据进程需要进行最小的业务初始化。

不同进程共同的初始化业务逻辑

1
2
3
4
5
6
7
8
9
public class AppInitialization {

/**
* 不同进程共同的初始化代码
* @param application
*/
public void onAppCreate(Application application) {
// init
}

简单工厂模式

根据进程名进行对应进程的初始化逻辑。

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
public class AppInitFactory {

public static AppInitialization getAppInitialization(String processName) {
AppInitialization appInitialization;
if (processName.endsWith(":second")) {
appInitialization = new SecondApplication();
} else if (processName.endsWith(":third")) {
appInitialization = new ThirdApplication();
} else {
appInitialization = new AppInitialization();
}
return appInitialization;
}

static class SecondApplication extends AppInitialization {

@Override
public void onAppCreate(Application application) {
super.onAppCreate(application);
// init
}
}

static class ThirdApplication extends AppInitialization {
@Override
public void onAppCreate(Application application) {
super.onAppCreate(application);
// init
}
}

具体调用代码

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
public class MyApplication extends Application {

private static final String TAG = "MyApplication";

@Override
public void onCreate() {
super.onCreate();
String currentProcessName = getCurrentProcessName();
Log.e(TAG, "currentProcessName : " + currentProcessName );
AppInitialization appInitialization = AppInitFactory.getAppInitialization(currentProcessName);
if (appInitialization != null) {
appInitialization.onAppCreate(this);
}
}

/**
* 获取当前进程名称
* @return
*/
private String getCurrentProcessName() {
String currentProcessName = "";
int pid = android.os.Process.myPid();
ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
if (processInfo.pid == pid) {
currentProcessName = processInfo.processName;
break;
}
}
return currentProcessName;
}
}

Android 编码规范

简单总结了 Android 开发中的一些代码规范,供开发者参考。

0 Index

1 命名规范

大驼峰命名(UpperCamelCase):每个单词的第一个字母都大写。

小驼峰命名(lowerCamelCase):除第一个单词以外,每一个单词的第一个字母大写。

命名的基本原则

  • 不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
  • 严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。但比如 shanghai 等通用的名称,可视同英文。
  • 除了常见的英文缩写,尽量避免缩写。

1.1 类 / 接口命名

  • 使用大驼峰命名法,用名词或者名词词组命名,每个单词的首字母大写。
  • 尽量避免大写,除非该缩写是众所周知的,比如 URLHTML 等。
  • 接口命名规则与类一样采用大驼峰命名法,多以ableible 结尾,如interface Runnableinterface Accessible
  • 若项目采用 MVP 架构,接口都以 I 为前缀,不加后缀,其他的接口采用上述命名规则。比如 interface IUserTest
  • 常见类名命名规则如下:
描述 举例
Activity 类 以 Activity 为后缀 登录页面类 LoginActivity
Fragment 类 以 Fragment 为后缀 新闻标题列表 NewsTitlelFragment
Service 类 以 Service 为后缀 下载服务 DownloadService
Adapter 类 以 Adapter 为后缀 新闻详情适配器 NewsDetailAdapter
工具方法类 以 Utils 或 Manager 为后缀 日志工具 LogUtils
数据库类 以 DBHelper 为后缀 新闻数据库 NewsDBHelper
解析类 以 Parser 为后缀 JSON 解析类 JsonParser
BroadcastReceiver 类 以 Receiver 为后缀 强制下线广播 ForceOfflineReceiver
自定义共享基础类 以 Base 为前缀 BaseActivity、BaseFragment
测试类 以它要测试的类的名称开始,以 Test 结束 UserTest
抽象类 以 Abstract 或 Abs 为前缀 AbsUserTest

1.2 变量命名

  • 成员变量 / 局部变量
    • 使用小驼峰命名。
    • 不推荐使用谷歌的前面加 m 的编码风格。
  • 控件变量
    • 使用小驼峰命名。
    • 建议使用 控件缩写 + 逻辑名称 格式,例如 btnLoginetUserName
    • 对应的控件 id 命名为 控件缩写_逻辑名称,例如 btn_loginet_user_name
    • 常见控件缩写如下:
控件 缩写 控件 缩写
TextView tv ImageButton ib
EditText et CheckBox cb
WebView wv RadioButton rb
ImageView iv SeekBar sb
VideoView vv ProgressBar pb
MediaController mc Spinner spr
ListView lv SerachView sev
GridView gv Button btn
Gallery gly TimePicker tp
LinearLayout ll DatePicker dp
RelativeLayout rl ScrollView sv
FrameLayout fl RecyclerView rv

1.3 常量命名

  • 单词每个字母均大写。
  • 单词之间用下划线连接,力求语义表达完整清楚,不要嫌名字长。

1.4 方法命名

  • 使用小驼峰命名。
  • 方法名通常是动词或动词短语,保证见名知义,尽量不适用 or 或者 and,遵循 “ do one thing ” 原则。
  • 一个方法尽量不要超过 50 行,如果方法太长,说明当前方法业务逻辑已经非常复杂,那么就需要进行方法拆分。
方法 说明 方法 说明
initXX() 初始化相关方法 resetXX() 重置数据
onXX() 回调方法 clearXX() 清除数据
getXX() 具有返回值的获取方法 removeXX() 移除数据或视图
setXX() 设置方法 drawXX() 绘制
isXX()/hasXX()/checkXX() 布尔型的判断方法 displayXX()/showXX() 展示、提示信息的方法
updateXX() 更新数据 handleXX()/processXX() 对数据进行处理的方法
saveXX() 保存数据

1.5 资源文件命名

全部小写,采用下划线命名法。

1.5.1 布局文件命名(xml 文件)

以对应的类别名称为前缀,逻辑名称在后,以下划线连接。

布局类型 布局前缀
Activity activity_
Fragment fragment_
Include include_
Dialog dialog_
PopupWindow popup_
Menu menu_
GridView 的item 布局文件 item_grid_
ListView 的 item 布局文件 item_list_

1.5.2 drawable 文件命名

以用途缩写作为前缀,逻辑名称在后,以下划线连接,区分状态时,添加状态后缀。

drawable 规则
图标资源 ic_
背景图片 bg_
按钮图片 btn_
分隔线 div_
默认类 def_
区分状态时,默认状态 _normal
区分状态时,按下时的状态 _pressed
区分状态时,选中时的状态 _selected
区分状态时,不可用时的状态 _disable
区分状态时,悬停效果 _hovered
区分状态时,可选状态 _checkable

1.5.3 动画文件命名(anim 文件夹下)

规则:动画类型_动画方向。

名称 说明
fade_in 淡入
fade_out 淡出
push_down_in 从下方推入
push_down_out 从下方推出
push_left 推向左方
slide_in_from_top 从头部滑动进入
zoom_enter 变形进入
slide_in 滑动进入
shrink_to_middle 中间缩写

1.5.4 values 中 name 命名

1.5.4.1 strings.xml
  • 命名格式:xx 界面 + 逻辑功能 ,如 activity_home_welcome_str
  • 建议把同一个界面的所有 String 都在一起,方便查找。
1.5.4.2 styles.xml
  • 使用大驼峰命名法,主题可以命名为XXTheme ,控件的风格可以命名为 XXStyle
1.5.4.3 colors.xml
  • 命名格式:color_16进制颜色值
  • 不要为某个控件指定特定颜色,比如 bg_login ,这样非常容易重复定义颜色值。
1
2
3
4
5
6
<resources>

<color name="red_FF432F">#FF432F</color>
<color name="red_FF0000">#FF0000</color>

</resources>
1.5.4.4 dimens.xml
  • 可以和 colors.xml 中命名格式类似
  • 必要时也可用”逻辑名称_功能“ 命名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<resources>

<!-- font sizes -->
<dimen name="font_22">22sp</dimen>
<dimen name="font_12">12sp</dimen>

<!-- typical spacing between two views -->
<dimen name="spacing_40">40dp</dimen>
<dimen name="spacing_4">4dp</dimen>

<!-- typical sizes of views -->
<dimen name="button_height_60">60dp</dimen>
<dimen name="button_height_40">40dp</dimen>

</resources>

2 注释规范

类、类属性、类方法的注释必须使用 Javadoc 规范,使用/** XXX */ 格式,不得使用 // XXX 方式。

2.1 类和接口注释

类和接口统一添加 Javadoc 注释,要求至少写出创建者、创建时间以及内容简要说明。具体可以在 AS 中自己配制,Settings → Editor → File and Code Templates → Includes → File Header,格式如下:

1
2
3
4
5
6
/**
* author : xxx
* e-mail : xxx@xx
* time : 2017/08/28
* desc :
*/

2.2 方法注释

下面几种方法,都必须添加 Javadoc 注释,除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。

  • 接口中定义的所有方法
  • 抽象类中自定义的抽象方法
  • 抽象父类的自定义公用方法
  • 工具类的公用方法
1
2
3
4
5
6
7
/** 方法的一句话概述
* 方法详述(简单方法可不必详述)
* @param s 说明参数含义
* @return 说明返回值含义
* @throws IOException 说明发生此异常的条件
* @throws NullPointerException 说明发生此异常的条件
*/

2.3 变量和常量注释

下面几种情况下的常量和变量,都要添加注释说明,优先采用右侧 // 来注释,若注释说明太长则在上方添加注释。

  • 接口中定义的所有常量
  • 公有类的公有常量
  • 枚举类定义的所有枚举常量
  • 实体类的所有属性变量

2.4 方法体内代码的注释

  • 方法内部单行注释,在被注释语句上方另起一行,使用 // 注释。
  • 方法内部多行注释使用 /* ... */ 注释。
  • 注意与代码对齐,*// 与其后文字之间空一格。
  • 不要在方法内部使用 Javadoc 形式的注释。

2.5 其他一些注释

  • 资源文件代码注释
1
<!-- 注释内容 -->
  • AS 已帮你集成了一些注释模板,我们只需要直接使用即可,在代码中输入todofixme等这些注释模板,回车后便会出现如下注释:
1
2
// TODO: 2017/8/28 需要实现,但目前还未实现的功能的说明
// FIXME: 2017/8/28 需要修正,甚至代码是错误的,不能工作,需要修复的说明

3 格式规范

3.1 使用标准的大括号风格

  • 大括号与if, else, for, do, while语句一起使用,即使只有一条语句(或是空),也应该把大括号写上。
  • 对于非空块和块状结构,大括号遵循 Kernighan 和 Ritchie 风格(R & N)
    • 左大括号前不换行
    • 右大括号前换行
    • 如果右大括号是一个语句、函数体或类的终止,则右大括号后换行;否则不换行。例如,如果右大括号后面是 elsecatch,则不换行。
1
2
3
4
5
6
7
8
9
10
11
class MyClass {
public void method() {
if (condition()) {
try {
something();
} catch (ProblemException e) {
recover();
}
}
}
}
  • 一个空的块状结构里什么也不包含,大括号可以简洁地写成{},不需要换行。

3.2 限制代码行的长度

  • 每行代码的长度不应该不超过 100 个字符。
  • 例外:packageimport语句,不换行。
  • 例外:注释中包含了超过 100 个字符的命令示例或者 URL,为了便于剪切和复制,其长度可以超过 100 个字符。

3.3 合理空白

  • 垂直空白
    • 方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行。
  • 水平空白
    • 左小括号和右小括号与字符之间不出现空格。
    • if/for/while/switch/do 等保留字与括号之间都必须加空格。
    • 任何二目、三目运算符的左右两边都需要加一个空格。
    • 方法参数在定义和传入时,多个参数逗号后边必须加空格。
    • 请使用快捷键 ctrl + alt + L 格式化代码。

4 参考资料

Android Intent 传递自定义对象

Intent 可以用来启动活动、发送广播、启动服务等,通过 putExtra 方法可以添加一些附加数据,达到传值的效果,但若想传递自定义对象的时候就无能为力了。

可以通过使用 Serializable 接口、Parcelable 接口以及转换对象为字符串的方式进行传递。

1、Serializable

表示将一个对象转为字节实现可存储或可传输的状态,一个对象能够序列化的前提是实现 Serializable 接口。

Model:

1
2
3
4
5
6
7
8
9
10
11
12
public class Person implements Serializable {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

传递对象:

1
2
3
4
5
Person person = new Person();
person.setName("wuzy");
Intent intent = new Intent(MainActivity.this, Main2Activity.class);
intent.putExtra("person_data",person);
startActivity(intent);

接收对象:

1
Person person = (Person) getIntent().getSerializableExtra("person_data");

2、Parcelable

将一个完整的对象进行分解,分解后的每一部分都是 Intent 所支持的数据类型。

Model:

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
public class Person implements Parcelable {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public int describeContents() {
return 0;
}

/*
将想要传递的数据写入到 Parcel 容器中
*/
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(name);
}

public static final Creator<Person> CREATOR = new Creator<Person>() {
/*
用于将写入 Parcel 容器中的数据读出来,用读出来的数据实例化一个对象,并且返回
*/
@Override
public Person createFromParcel(Parcel in) {
Person person = new Person();
person.setName(in.readString());
return person;
}

/*
创建一个长度为 size 的数组并且返回,供外部类反序列化本类数组使用
*/
@Override
public Person[] newArray(int size) {
return new Person[size];
}
};
}

传递对象:

1
2
3
4
5
Person person = new Person();
person.setName("wuzy");
Intent intent = new Intent(MainActivity.this, Main2Activity.class);
intent.putExtra("person_data",person);
startActivity(intent);

接收对象:

1
Person person =  getIntent().getParcelableExtra("person_data");

3、转化为 JSON 字符串

Model:

1
2
3
4
5
6
7
8
9
10
public class Person {
private String name;
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

传递对象:

1
2
3
4
5
Person person = new Person();
person.setName("wuzy");
Intent intent = new Intent(MainActivity.this, Main2Activity.class);
intent.putExtra("person_data",new Gson().toJson(person));
startActivity(intent);

接受对象:

1
2
String personJson = getIntent().getStringExtra("person_data");
Person person = new Gson().fromJson(personJson, Person.class);

速度上,使用Parcelable 速度最快,Serializable 次之,转换为 JSON 字符串最慢。