第五周总结

27 篇文章 0 订阅
23 篇文章 1 订阅

时间: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);
}

四、ListView 缓存机制

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值