时间:2019 年 01 月 28 日 ~ 2019 年 02 月 01 日
第五周总结
一、LayoutInflater 三参方法含义及工作流程
1.三参方法含义
View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
resource:要加载的 XML 布局资源的资源 ID
root:是否将要加载的 XML 布局资源生成的层次结构添加到根可选视图 root 上
attachToRoot:
① root 不为空
若为 true,则根可选视图 root 将成为 XML 布局资源生成的层次结构的父级
若为 false,则只是为 XML 布局资源生成的层次结构的根,生成其对应的 layoutParams
② root 为空
XML 布局资源生成的层次结构的根节点宽高失效,因为宽高是由 root 参与生成的,root 为空的情况下,是否 attachToRoot 则没有区别
2.工作流程
由获取 LayoutInflater 的代码可知,LayoutInflater 真正的实现类是 PhoneLayoutInflater
LayoutInflater#from():
public static LayoutInflater from(Context context) {
// context 的实现类是 ContextImpl
LayoutInflater LayoutInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
ContextImpl#getSystemService():
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
所有的系统服务都在 SystemServiceRegistry 中注册,搜索一下 LAYOUT_INFLATER_SERVICE
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
// 根据 LAYOUT_INFLATER_SERVICE,系统返回的是 PhoneLayoutInflater
return new PhoneLayoutInflater(ctx.getOuterContext());
}
}
);
PhoneLayoutInflater:LayoutInflater 的实现类
public class PhoneLayoutInflater extends LayoutInflater {
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
...
/**
* 根据控件名称实例化系统内置控件
*/
@Override
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
...
}
LayoutInflater#inflate():
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
LayoutInflater#inflate():
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
// 关键一:使用 createViewFromTag() 方法解析布局文件
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
// Inflate all children under temp against its context.
// 关键二:递归解析子控件
rInflateChildren(parser, temp, attrs, true);
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
...
} catch (Exception e) {
...
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
return result;
}
}
① 解析传入的布局
② 解析布局根标签,大部分情况对应 createViewFromTag() 方法解析
③ 调用 rInflateChildren() 方法,递归解析根标签下的子 View
LayoutInflater#createViewFromTag():解析布局文件
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
...
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
try {
View view;
// 使用 mFactory2、mFactory 去解析生成控件
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
// indexOf() 返回 -1,表示 name 中不含点
// 当前控制名不含点符合,对应系统内置控件的名称
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else { // 当前控制名含点符合,对应自定义控件的名称
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
...
} catch (ClassNotFoundException e) {
...
} catch (Exception e) {
...
}
}
① 使用一系列 Factory 解析生成控件,生成不为空就返回
② 使用 onCreateView() 或 createView() 解析生成控件
解析系统内置控件,使用 onCreateView()
解析自定义控件,使用 createView()
二、进程和线程、创建线程方式、线程生命周期
3.1 进程和线程
根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过 CPU 调度,在每个时间片中只有一个线程执行)
内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了 CPU 外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源
包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程
补充:线程是 CPU 调度和分配的基本单位。线程可以拥有自己的操作栈、程序计数器、局部变量表等资源,它与同一进程内的其他线程共享该进程的所有资源
3.2 线程的创建有三种方式
① 继承 Thread
② 实现 Runnable 接口
③ 使用 Callback 接口
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "";
}
}
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
new Thread(futureTask).start();
// get():阻塞方法,再返回结果之前会一直阻塞
System.out.println(futureTask.get());
}
Callable 接口和 Runnable 接口区别:
Callable 实现方法为 call(),Runnable 实现方法为 run()
Callable#call() 方法可以有返回值,Runnable#run() 方法没有返回值
Callable#call() 方法可以抛异常,Runnable#run() 方法不能抛异常
运行 Callable 任务后,可以根据对应的 Future 对象(FutureTask 中对任务的操作的方法都在 Future 接口中定义)获取 Callable 任务是否执行完成,可以取消任务,获取任务执行结果
3.3 线程生命周期
Java多线程学习(三)—线程的生命周期
① 新建(New)
使用 new 关键字创建线程后,该线程就处于新建状态,当前状态和其它 Java 对象一样,仅仅由 Java 虚拟机会为其分配内存,并初始化其成员变量的值,此时线程对象还没有表现出线程的动态特征,程序也不会执行线程的线程执行体
② 就绪(Runnable)
调用 start() 方法(只需被调用一次就会进入就绪状态,再次调用会抛异常)后,线程就处于就绪状态,Java 虚拟机会为其创建方法栈和程序计数器,此时线程是可以运行的,但是没有开始运行,何时开始运行取决于 Java 虚拟机的线程调度器的调度
// 注意:只有调用 start() 方法,然后系统才会把 run() 当做线程执行体来运行,不要直接调用 run(),这样的话,此时 run() 方法只是一个普通方法,会被立即执行,相当于在线程对象所在线程执行,之后的操作在 run() 返回前不会执行,因为代码按顺序执行
③ 运行(Running)
获取 CPU 后,线程开始执行,此时处于运行状态
④ 阻塞(Block)
调用 sleep()、join() 等主动放弃 CPU 执行权
调用 wait()
视图获取同步监视器,但监视器被其它线程持有
调用阻塞式 IO 方法,方法返回前,线程被阻塞
调用 suspend() 后,线程被挂起(容易导致死锁,避免使用)
// 注意:线程被阻塞后,之后再次运行时,需要先进入就绪状态,即必须等待线程调度器的调度
⑤ 死亡(Dead)
run() 或 call() 方法执行完成
线程抛出未捕获的 Exception 或 Error
调用 stop() 方法结束线程(容易导致死锁,不推荐使用)
// 注意:线程死亡后,不能再次调用 start() 方法,否则抛异常
三、ScrollView 包裹 ListView 只显示一行的原因及解决方法
先查看 ScrollView#measureChildWithMargins():
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
// 子控件的高度模式设置成 MeasureSpec.UNSPECIFIED
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
ScrollView#measureChildWithMargins() 中,ScrollView 会把子控件的高度模式设置成 MeasureSpec.UNSPECIFIED
再查看 ListView#onMeasure() 方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
// ListView 高度测量模式为 UNSPECIFIED
if (heightMode == MeasureSpec.UNSPECIFIED) {
// ListView 高度测量大小近似等于一行的高度 childHeight
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
// 若 ListView 高度测量模式为 AT_MOST
if (heightMode == MeasureSpec.AT_MOST) {
// measureHeightOfChildren() 方法测量 ListView 的高度
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
当 ListView 高度测量模式为 UNSPECIFIED 时,ListView 高度测量大小近似等于一行的高度 childHeight
综合所述,可得 ListView 显示一行的原因如下:
① ScrollView 会把子控件的高度模式设置成 MeasureSpec.UNSPECIFIED
② 当 ListView 高度测量模式为 UNSPECIFIED 时,ListView 高度测量大小近似等于一行的高度 childHeight
解决办法:
修改 ListView 高度的测量模式为 AT_MOST,让 ListView 使用 measureHeightOfChildren()
方法完成高度的测量
ListViw#measureHeightOfChildren():
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, int maxHeight, int disallowPartialChildPosition) {
...
// Include the padding of the list
int returnedHeight = mListPadding.top + mListPadding.bottom;
final int dividerHeight = mDividerHeight;
...
for (i = startPosition; i <= endPosition; ++i) {
child = obtainView(i, isScrap);
measureScrapChild(child, i, widthMeasureSpec, maxHeight);
if (i > 0) {
// Count the divider for all but one child
returnedHeight += dividerHeight;
}
...
returnedHeight += child.getMeasuredHeight();
// returnedHeight:需要返回的高度
// maxHeight:对应当前方法的第四个参数,由 onMeasure() 方法中的 ListView 的 heightSize 传入
// returnedHeight(返回的高度) > heightSize(最大高度),则之后的视图不会被完全填充
// 因此 heightSize 值取尽可能的大(Integer.MAX_VALUE >> 2),ListView 的高度就能测量完全
if (returnedHeight >= maxHeight) {
return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
&& (i > disallowPartialChildPosition) // We've past the min pos
&& (prevHeightWithoutPartialChild > 0) // We have a prev height
&& (returnedHeight != maxHeight) // i'th child did not fit completely
? prevHeightWithoutPartialChild
: maxHeight;
}
if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
prevHeightWithoutPartialChild = returnedHeight;
}
}
// 这里返回的高度就是完全测量的高度
return returnedHeight;
}
自定义类继承 ListView,重写 onMeasure() 方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 高度大小:Integer.MAX_VALUE >> 2(范围内的最大值)
// 高度模式:MeasureSpec.AT_MOST
heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
// 覆盖系统的 heightMeasureSpec
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}