把把Android事件分发机制的脉络

发布时间:2021-11-29 05:05:41

开始的开始

当我手指触摸到手机屏幕后,给我所见即所得的触控体验,它的背后发生了什么?出于对它的好奇,就有了这篇文章。因为本人不喜欢大段大段的源码分析,所以本文尽量从设计的角度去把把事件分发机制的脉络,少贴源码是我的目标之一,虽然这么说,但当我把文章写完后最后发现依然贴了不少代码,想删减却没法减少,都是很核心的部分。本博客的主要内容如下:


    事件分发的整体设计ViewRootImpl的创建应用层级的事件分发流程

为了后面更容易的理解事件分发机制,在介绍核心内容前,准备了一些前置知识。


前置知识:View树的创建过程

我们知道在Activity里的onCreate()方法里调用setContentView(R.layout.xxx)方法,就可以把我们的xml布局显示到activity上了。 所写的xml布局是树结构,最终形成的view可以理解成是一个view树枝。树枝是有了,要形成一个完整的树,不可或缺的是树根,那整个View树的根(或者说顶级View)是谁呢?


那接下来,我们走进它的源码一窥究竟。



为了让思路更简洁清晰,不受那些细节的影响,以下代码只摘录与本文最相关的部分,甚至你可以直接当成伪代码来看。



我们就从setContentView()作为入口:


#Activity
private Window mWindow
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public Window getWindow() {
return mWindow;
}

可以看到它又通过 getWindow()setContentView,这个mWindow具体的实例对象是PhoneWindow,它会在Activity 的启动过程,通过attch()创建。


#ActivityThread
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
activity.attach(...);
}
#Activity
attch(...){
mWindow = new PhoneWindow(this, window, activityConfigCallback);
}

接下来走进PhoneWindowsetContentView()


#PhoneWindow
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
}
...
mLayoutInflater.inflate(layoutResID, mContentParent);
}

mLayoutInflater.inflate()函数我们就很熟悉了,它会把布局layoutResID填充到根布局mContentParent里面


mContentParent这个view是什么呢,怎么来的呢?可以从installDecor()函数去看:


#PhoneWindow
private void installDecor() {
if (mDecor == null) {
//生成DecorView
mDecor = generateDecor(-1);
}
...
if (mContentParent == null) {
//生成mContentParent
mContentParent = generateLayout(mDecor);
}
}

//生成DecorView
protected DecorView generateDecor(int featureId) {
return new DecorView(...)
}
//DecorView是继承FrameLayout的view
public class DecorView extends FrameLayout{}

//生成mContentParent
protected ViewGroup generateLayout(DecorView decor) {
TypedArray a = getWindowStyle();
...
int layoutResource;
//会根据一些设置去配置布局
if ((....) {
layoutResource = ....
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
//比如这里我们常设置不需要Title的Activity就会走这里 requestWindowFeature(Window.FEATURE_NO_TITLE);
layoutResource = R.layout.screen_title;
}else{
//如果没有设置的话
layoutResource = R.layout.screen_simple;
}
//将layoutResource加载到DecorView去
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//ID_ANDROID_CONTENT = com.android.internal.R.id.content
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
return contentParent;
}

最终返回结果contentParent,它是id为R.id.content的ViewGroup。那这个id从哪个布局获取到的呢,可以猜测出默认布局R.layout.screen_simple会有这个id:


android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />


从布局中可以看到,contentParent是个idcontentFrameLayout


到这里我们就可以得到结论: 我们在ActivityonCreate()方法里常写的setContentView(layoutResID)最终会填充到FrameLayout的布局中去。而这个FrameLayout是放在一个竖直方向的LinearLayout里面。


那这整个以LinearLayout为最外层的layout要放在哪里呢?


//将layoutResource加载到DecorView去
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
public class DecorView extends FrameLayout
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
...
final View root = inflater.inflate(layoutResource, null);
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
...
}
}

从这段代码可以看出 ,contentParent最终会通过addView方法填充到DecorView中去。


到这里可以小结一下:在Activity里设置的布局会添加到名为contentParentview上,而contentParent会添加到DecorView中,而DecorViewPhoneWindow下的View,所以它也顺理成章成为了整个View树的顶级View。从这里也可以得出一个结论:Activity它并负责视图控制,真正控制试图的是PhoneWindow



可以思考一个问题:这样看没有Activity的存在也是可以的,那Activity被设计的目的是什么呢?



为了可以更直观的展示上面的结论,上图:



提这个前置知识最重要有两个目的:


    对整个View树的构建过程有个了解

    View树的顶级View是DecorView


知道这两点后可以帮助我们更容易的理解后面的核心内容。


事件分发整体的设计

事件分发的源头往往是我们触摸屏幕(包含手指、笔、鼠标等),它最开始是一个物理层面的触摸输入事件,然后通过一系列转换过程到我们应用上层。作为应用层开发者,更直接关注到的肯定还是应用层面的代码。


那假设这个触摸输入事件已经到我们的应用层了,那我们从应用层角度来设计的话,会如何设计?


我们是否只要定义这样一个类MotionInputEvent去接收事件并处理就可以了?看命名还挺贴切的。其实仔细想想,除了触摸输入事件外,常用的还有键盘输入,所以只定义这样一个类从设计上是不合理的,没有考虑到它的可扩展性。


因此定义一个输入事件抽象层InputEvent,让触摸输入事件MotionEvent、键盘输入KeyEvent都继承该抽象类,这才是一个合理的设计。看看源码确实是这么设计的:


#package android.view;
public abstract class InputEvent implements Parcelable{}
public final class MotionEvent extends InputEvent implements Parcelable {}
public class KeyEvent extends InputEvent implements Parcelable {}

既然有了这两种输入事件类型,肯定还需要一个地方管理它们:在接收到输入事件的时候进行管理,然后再分发给具体的输入事件类来处理。这个类就是系统输入管理器InputManagerInputManager作为管理分发InputEvent的地方,那它是在哪里被创建出来的呢?


我们知道手机启动后,手机就要响应我们的输入事件,所以可以猜测出在系统启动时,和它相关的某个类应该就要创建出来。查看frameworks层的SystemServer类,找到那个最直接相关的类,发现是 InputManagerService ,看着是不是有点熟悉。看它长得就像我们熟知的 ActivityManagerServiceWindowManagerService一样,所以它在SystemServer启动时应该就会被创建出来。


#frameworks/base/services/java/com/android/server/SystemServer.java
public final class SystemServer {
private void run() {
startOtherServices()
}

private void startOtherServices() {
...
InputManagerService inputManager = null
inputManager = new InputManagerService(context);
//这里可以看出,InputManagerService和WindowManagerService有紧密的联系,InputManagerService的实例直接传给了WindowManagerService的main方法
wm = WindowManagerService.main(context, inputManager, !mFirstBoot, mOnlyCore,
new PhoneWindowManager(), mActivityManagerService.mActivityTaskManager);
inputManager.start();
...
}
}

#frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
public class InputManagerService extends IInputManager.Stub
implements Watchdog.Monitor {

public InputManagerService(Context context) {
mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
}
//调用native方法初始化InputManagerService
private static native long nativeInit(InputManagerService service,
Context context, MessageQueue messageQueue);
}

有意思的是,从源码可以看到InputManagerServiceWindowManagerService居然建立了联系,这是我们一开始没有想到的。不过细想琢磨一下,如果对WindowManagerService 熟悉的话,会发现这样的设计是理所当然的:


WindowManagerService是窗口的大主管(下面简称WMS),它记录了当前系统中所有窗口的完整信息,所以只有它才能判断出要把输入的事件投递给具体的某个应用进程进行处理。



当然具体怎么传递的就涉及细节。不用理会它们,在考虑整体设计的时候,具体细节都可暂时跳过,不能被它们蒙蔽了双眼,阻碍了前进的脚步。 这些细节点,可在事后找时间逐一击破它们。



到这里我们可以小结一下:在系统启动时,SystemServer会启动窗口管理服务WindowManagerServiceWindowManagerService在启动的时候就会通过InputManagerService,启动Native层的InputManager来接收硬件层的输入事件。接收到输入事件后,WindowManagerService会经过判*咽淙胧录址⒏掣鼍咛宓挠τ媒獭


WMS不仅是是窗口的大主管,还是InputEvent的派发者


那么现在的问题就转变成:WMS分发输入事件InputEvent后,应用进程如何接收的了?这里的应用进程可以理解成是一个应用层级的窗口Window。因为事件输入的目的地是应用层级的窗口Window


我们可以想到的是在WMS应用窗口Window中间肯定需要一个纽带或者说是中介,去衔接这两者。那这个纽带是谁呢?


对View绘制比较了解的人应该很熟悉ViewRootImpl。View的测量、布局、绘制都由它控制。它作为整个View树的根部,是View树正常运作的基石。后面的分析过程也会提到这一点的。


于此同时,ViewRootImpl就是WMS应用窗口Window建立通信的纽带。既然是纽带,那么必然有两者的依赖,所以它的创建过程就非常重要了,按理说是可以从中找到依据的。


ViewRootImpl的创建

ViewRootImpl是在哪里被创建的呢?这就要从handleResumeActivity流程看起


#ActivityThread
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,String reason) {
r.window = r.activity.getWindow();
ViewManager wm = a.getWindowManager(); //这里的wm实例对象就是WindowManagerImpl
WindowManager.LayoutParams l = r.window.getAttributes();
wm.addView(decor, l);
}

wm.addViewwm的实例对象就是WindowManagerImpl,其中的参数decorDecorView对象,在前置知识ViewTree的创建过程中已经提过了。


所以接着看WindowManagerImpladdView方法做了什么?


#WindowManagerImpl
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

又调用到 WindowManagerGlobaladdView方法


#WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
ViewRootImpl root;
root = new ViewRootImpl(view.getContext(), display); //创建了ViewRootImpl对象
mViews.add(view); //记录DecorView
mRoots.add(root); //记录ViewRootImpl
Params.add(wparams); //记录WindowManager的LayoutParams
root.setView(view, wparams, panelParentView);
}

到这里可以先小结一下:在ActivityThread.handleResumeActivity()流程中, 通过WindowManager(WindowManagerImpl)addView()实现了ViewRootImpl的创建。 此时我们应用层窗口 Window就和ViewRootImpl就建立了关联。



从代码中可以看到,除了ViewRootImpl的创建还会把构建出来的ViewRootImplDecorViewWindowManager.LayoutParams记录下来,用几个数组分别存储,它们是一一对应的。



那么,现在应用层窗口 Window就和ViewRootImpl就建立了关联,还剩一个问题是 WMS 和 ViewRootImpl怎么建立关联的呢?


继续往下看源码,进入ViewRootImpl 的 setView 方法,我们就能找到答案了。看看它做了什么?


#ViewRootImpl
final IWindowSession mWindowSession
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
// 控制测量(measure)、布局(layout)、绘制(draw)的开始
requestLayout();
//包括了发送和接收消息的功能封装
mInputChannel = new InputChannel();
//通过Binder调用,进入系统进程的Session,调用WMS的addWindow方法
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
//创建事件输入处理接收者
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
...
}

mWindowSession.addToDisplay()函数是添加窗口流程,对应的服务端就是WMS,而WMS又是个系统进程,所以这是个Binder跨进程调用方法,最终调用的是WMSaddWindow方法。而参数mInputChannel,它包括了发送和接收消息的功能封装。


至此这里已经验证了我们想要的结果了,ViewRootImpl确实是应用层级WindowWMS的建立了通信的纽带。


小插入:前面提到ViewRootImpl是View绘制的根基。为什么这么说?


可以从setViewrequestLayout()看出。在requestLayout()方法,它主要做了两件事情:


    检查线程开始测量(measure)、布局(layout)、绘制(draw)

#ViewRootImpl
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//在同步信号过来的时候, mTraversalRunnable的run函数将调用
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}

void doTraversal() {
performTraversals();
}

private void performTraversals() {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)
performLayout(lp, mWidth, mHeight);
performDraw();
}

应用层级的事件分发流程

现在WMS和应用层窗口了已经有了通信的条件。输入事件可以从底层硬件分发到应用层窗口了。发是发过来了,可还需要一个接收者呀。如果你细心的话,或许会在上面代码里有所发现。在setView方法的最后,创建了一个WindowInputEventReceiver对象。它的职责就是接受输入事件的。


final class WindowInputEventReceiver extends InputEventReceiver {
@Override
public void onInputEvent(InputEvent event) {
//将事件加入队列
enqueueInputEvent(event, this, 0, true);
}
}

void enqueueInputEvent(InputEvent event, InputEventReceiver receiver,
int flags, boolean processImmediately) {
//将事件加入到队列中
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
if (last == null) {
mPendingInputEventHead = q;
mPendingInputEventTail = q;
} else {
last.mNext = q;
mPendingInputEventTail = q;
}
}


事件是接收完了,那要怎么处理呢?


当事件被接收了,按正常的逻辑就应该要开始处理了,也就是说在WindowInputEventReceiver对象创建成功后,就应该要处理。所以推理在该对象成功之后就有这部分处理逻辑。再次查看setView()方法:


public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
// Set up the input pipeline. 创建输入链
CharSequence counterSuffix = attrs.getTitle();
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
}

这里它使用了责任链设计模式。对于不同的输入事件需要使用相应的处理方法,而这些处理方法需要有优先级,所以通过责任链模式,把不同的处理阶段通过一条链串起来是一个很优雅的设计实现。


那这些处理阶段都是怎么处理的呢,比如我们挑一个ViewPostImeInputStage来看下:


/**
* Delivers post-ime input events to the view hierarchy.
*/
final class ViewPostImeInputStage extends InputStage {
public ViewPostImeInputStage(InputStage next) {
private int processKeyEvent(QueuedInputEvent q) {...}
private int processGenericMotionEvent(QueuedInputEvent q) {...}
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
//mView就是DecorView,让顶层的事件开始分发
boolean handled = mView.dispatchPointerEvent(event);
}
}

ViewPostImeInputStage它是视图输入处理阶段 ,它的主要职责在这个方法的注释中写的很简洁明了: 将输入事件分发传递到视图层次结构


代码中由DecorView这个顶级View调用dispatchPointerEvent(event)进行分发。所以这里也就成为View层级的事件分发 流程的起点。



上面的事件已经分发到了顶级View,即DecorView上了。作为事件分发头儿。那它首先要分给谁,怎么分发就很重要了。我们知道DecorView它是继承自FrameLayout的,所以它是个ViewGroup,那么它会直接分发给它的child-View吗?还是会传递给给它的父View-ViewGroup呢?


还是得从它的dispatchTouchEvent 看起


#DecorView
public class DecorView extends FrameLayout{
//1.分发给Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
//这里的Callback接口方法会在Activity里实现
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
}
#Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
//2.分发给Window(即PhoneWindow)
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
#PhoneWindow
public boolean superDispatchTouchEvent(MotionEvent event) {
//3.分发给DecorView
return mDecor.superDispatchTouchEvent(event);
}
#DecorVew
public boolean superDispatchTouchEvent(MotionEvent event) {
//4.分发给ViewGroup
return super.dispatchTouchEvent(event);
}
#ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
//5.分发给View
super.dispatchTouchEvent(event);
}

从代码中可以看出,DecorView虽然是ViewGroup,但它并不会直接分发给它的child,也不会传递给它的父View,而是先分发给Activity,为什么要这么做呢?



我们可想DecorView拿到事件后,若直接向下分发,那就没Activity的事了,但Activity也想成为事件分发的一环,并还想作为优先级较高的部分。所以就可以通过这种方式,绕了一圈又回到DecorView并再往下分发。DecorView在这里承担两个职责:


    在接收到输入事件时,它不同于普通View,分发给子view,而是会先将事件分发给上层的Activity接收到上层Activity分发下的事件后,又会变成普通View,进行ViewGroup的事件分发

ViewGroup的事件分发流程

对于ViewGroup的事件分发过程,以下这一段伪代码可以将其过程表达的淋漓尽致:


#ViewGroup
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
//拦截了事件
if (onInterceptTouchEvent(ev)) {
consume = onToucheEvent(ev)
}else{
//有child,就分发给它
if (hasChild) {
consume = child.dispatchTouchEvent();
}
//如果没有child或没有消费就分发给自己
if (!consume) {
consume = super.dispatchTouchEvent();
}
}
return consume
}

ViewGroup的事件分发过程不是本文的主要内容,不再过多概述。


如果想深入这部分内容,建议阅读《Android开发艺术探索》的第三章,写的很详细。


或者看鸿洋大神的这篇博客:


ViewGroup事件分发机制


最后的最后

本文撰写花了挺久的时间,过程中参考了很多大佬的博客:


1.却把清梅嗅的反思系列


反思|Android 事件分发机制的设计与实现


2.任玉刚的《Android开发艺术探索》


3.InputStage 的深度好文:


Android Framework 输入子系统 (09)InputStage解读

相关文档

  • 诚实诚信的作文
  • 【iOS】iPhone系统常用文件夹位置
  • 个人一周学习计划范文_个人一周学习计划总结
  • 不排卵能做人工授精吗 一侧输卵管不通能做人工授精吗
  • 运维面试前准备
  • 比较分析法的形式是什么意思
  • 徘徊怎么造句
  • iPhone/iPad如何打开压缩文件rar.zip?
  • 秋日胭脂月季的优缺点
  • 关于做美甲的10大好处
  • 三个鬼是脑筋急转弯
  • 关于郭敬明的好句子
  • Linux基础知识(虚拟机的安装及控制)
  • 会计面试的自我介绍演讲稿范文
  • java面试被虐记录
  • 无花果叶子泡茶的功效与作用
  • 以微笑绵延春色
  • 完整的棋牌游戏开发流程,你知道多少棋牌游戏开发。
  • 搞笑说说心情短语大全
  • 个人工作计划锦集7篇
  • 华为p10手机无线网速慢怎么办
  • 玻璃编号辨别二手车方法
  • 数学小常识10字
  • 喝姜汤要学会“对症下汤”
  • 金融业实习生自我鉴定范文
  • 系统重装时巧防电脑病毒
  • 2020小学图书室的管理工作计划
  • 在windows7显示在窗口最顶部的称为
  • 2017雅思阅读是非判断题解题技巧
  • MySQL Explain详解
  • 猜你喜欢

  • 自贡市瑞峰物流有限公司企业信用报告-天眼查
  • 辽宁省能源矿产勘探行业企业研究报告2018版
  • 班组长的一天
  • 抗生素用於食用性动物对於人类健康的风险性评估
  • 黄体酮与间苯三酚分别联合绒毛膜促性腺激素在先兆流产治疗中的疗效对比
  • 商务英语必备英语词汇总结
  • 班组长一日管理
  • 广东2020年高考分数线:本科文430分 理410分
  • 2020年中班幼儿教师个人工作总结范文1200字
  • 终身体育视角下公共体育课程教学研究
  • 有关外国的名言警句
  • 创造性思维方法及产品创新方法
  • 脱贫攻坚个人心得体会600字多篇
  • Vue中组件生命周期调用顺序
  • 磨砂手机壳掉漆怎么办
  • 【政治】四川省成都七中实验学校2015-2016学年高一上学期期中考试试题
  • 论《小王子》的悲剧美
  • 会议记录册(辅导员)
  • 简谈小学语文教学中如何让学生自主探究学习
  • 2010公司人事主管工作计划
  • 课标版2019-2020学年重庆八中九年级上学期期中化学试卷B卷
  • 高一家长会精品课件42-38页精选文档
  • 高二年级5月份地理月考试卷(含答案)
  • 六一国际儿童节献词与六一国际儿童节节目主持词汇编
  • 2018-2024年中国石英晶体器件行业市场调查及“十三五”投资战略预测报告
  • 【创意教案】最新幼儿教育-幼儿园大班科学:我的配方(四篇)
  • 学前班下期家长会发言稿
  • 2008中国大学教育学本科A
  • 人教版六年级下册语文读后感
  • 高考语文总复*专题专练-识记现代汉语普通话常用字的字音3
  • 2019年江苏南通市中考语文试题卷附答案详析
  • 九寨沟冬季旅游攻略
  • 消除旅游疲劳的方法
  • 优选教育四年级下册语文课件蝙蝠和雷达ㄏ人教新课标、.ppt
  • 重庆三川家用电器公司企业信用报告-天眼查
  • 生活水泵房承接查验记录表
  • 最新人教版初三化学第六单元知识点归纳总结说课讲解
  • 20XX年春季学期小学德育工作总结
  • 六人行(老友记)第10季第11集剧本
  • 上海小学语文2年级下学期复*资料
  • 以说梦想为题目的随笔
  • 用EXCEL提高工效——以安钢三类物资需求计划汇总为例
  • 电脑版