2022年Android应用程序消息处理机制分析[收 .pdf

上传人:C****o 文档编号:39713716 上传时间:2022-09-07 格式:PDF 页数:29 大小:737.85KB
返回 下载 相关 举报
2022年Android应用程序消息处理机制分析[收 .pdf_第1页
第1页 / 共29页
2022年Android应用程序消息处理机制分析[收 .pdf_第2页
第2页 / 共29页
点击查看更多>>
资源描述

《2022年Android应用程序消息处理机制分析[收 .pdf》由会员分享,可在线阅读,更多相关《2022年Android应用程序消息处理机制分析[收 .pdf(29页珍藏版)》请在得力文库 - 分享文档赚钱的网站上搜索。

1、 Android 应用程序是通过消息来驱动的,系统为每一个应用程序维护一个消息队例,应用程序的主线程不断地从这个消息队例中获取消息(Looper),然后对这些消息进行处理(Handler),这样就实现了通过消息来驱动应用程序的执行,本文将详细分析Android 应用程序的消息处理机制。前面我们学习Android 应用程序中的Activity 启动(Android 应用程序启动过程源代码分析和 Android应用程序内部启动Activity 过程(startActivity)的源代码分析)、Service 启动(Android 系统在新进程中启动自定义服务过程(startService)的原理分

2、析 和 Android 应用程序绑定服务(bindService)的过程源代码分析)以及广播发送(Android 应用程序发送广播(sendBroadcast)的过程分析)时,它们都有一个共同的特点,当ActivityManagerService需要与应用程序进行并互时,如加载Activity 和 Service、处理广播待,会通过 Binder 进程间通信机制 来知会应用程序,应用程序接收到这个请求时,它不是马上就处理这个请求,而是将这个请求封装成一个消息,然后把这个消息放在应用程序的消息队列中去,然后再通过消息循环来处理这个消息。这样做的好处就是消息的发送方只要把消息发送到应用程序的消息队

3、列中去就行了,它可以马上返回去处理别的事情,而不需要等待消息的接收方去处理完这个消息才返回,这样就可以提高系统的并发性。实质上,这就是一种异步处理机制。这样说可能还是比较笼统,我们以Android 应用程序启动过程源代码分析一文中所介绍的应用程序启动过程的一个片断来具体看看是如何这种消息处理机制的。在这篇文章中,要启动的应用程序称为Activity,它的默认Activity 是 MainActivity,它是由Launcher 来负责启动的,而Launcher 又是通过ActivityManagerService来启动的,当ActivityManagerService为这个即将要启的应用程序准

4、备好新的进程后,便通过一个Binder 进程间通信过程来通知这个新的进程来加载MainActivity,如下图所示:它对应 Android 应用程序启动过程中的Step 30 到 Step 35,有兴趣的读者可以回过头去参考Android应用程序启动过程源代码分析一文。这里的Step 30 中的 scheduleLaunchActivity是名师资料总结-精品资料欢迎下载-名师精心整理-第 1 页,共 29 页 -ActivityManagerService通过 Binder 进程间通信机制 发送过来的请求,它请求应用程序中的ActivityThread执行 Step 34 中的 perfor

5、mLaunchActivity操作,即启动MainActivity的操作。这里我们就可以看到,Step 30 的这个请求并没有等待Step 34 这个操作完成就返回了,它只是把这个请求封装成一个消息,然后通过Step 31 中的 queueOrSendMessage操作把这个消息放到应用程序的消息队列中,然后就返回了。应用程序发现消息队列中有消息时,就会通过 Step 32 中的 handleMessage操作来处理这个消息,即调用 Step 33中的 handleLaunchActivity来执行实际的加载MainAcitivy类的操作。了解 Android 应用程序的消息处理过程之后,我们

6、就开始分样它的实现原理了。与 Windows 应用程序的消息处理过程一样,Android 应用程序的消息处理机制也是由消息循环、消息发送和消息处理这三个部分组成的,接下来,我们就详细描述这三个过程。1.消息循环在消息处理机制中,消息都是存放在一个消息队列中去,而应用程序的主线程就是围绕这个消息队列进入一个无限循环的,直到应用程序退出。如果队列中有消息,应用程序的主线程就会把它取出来,并分发给相应的Handler 进行处理;如果队列中没有消息,应用程序的主线程就会进入空闲等待状态,等待下一个消息的到来。在Android 应用程序中,这个消息循环过程是由Looper 类来实现的,它定义在frame

7、works/base/core/java/android/os/Looper.java文件中,在分析这个类之前,我们先看一下Android应用程序主线程是如何进入到这个消息循环中去的。在 Android 应用程序进程启动过程的源代码分析一文中,我们分析了Android 应用程序进程的启动过程,Android 应用程序进程在启动的时候,会在进程中加载ActivityThread类,并且执行这个类的main函数,应用程序的消息循环过程就是在这个main 函数里面实现的,我们来看看这个函数的实现,它定义在 frameworks/base/core/java/android/app/ActivityT

8、hread.java文件中:view plain1.publicfinalclass ActivityThread 2.3.4.publicstaticfinalvoid main(String args)5.6.7.Looper.prepareMainLooper();8.9.10.11.ActivityThread thread=new ActivityThread();12.thread.attach(false);13.14.15.16.Looper.loop();17.18.名师资料总结-精品资料欢迎下载-名师精心整理-第 2 页,共 29 页 -19.20.thread.detac

9、h();21.22.23.24.这个函数做了两件事情,一是在主线程中创建了一个ActivityThread实例,二是通过Looper 类使主线程进入消息循环中,这里我们只关注后者。首先看 Looper.prepareMainLooper函数的实现,这是一个静态成员函数,定义在frameworks/base/core/java/android/os/Looper.java文件中:view plain1.publicclass Looper 2.3.4.privatestaticfinal ThreadLocal sThreadLocal=new ThreadLocal();5.6.final M

10、essageQueue mQueue;7.8.9.10./*Initialize the current thread as a looper.11.*This gives you a chance to create handlers that then reference12.*this looper,before actually starting the loop.Be sure to call13.*link#loop()after calling this method,and end it by calling14.*link#quit().15.*/16.publicstati

11、cfinalvoid prepare()17.if (sThreadLocal.get()!=null)18.thrownew RuntimeException(Only one Looper may be created per thread);19.20.sThreadLocal.set(new Looper();21.22.23./*Initialize the current thread as a looper,marking it as an applications main 24.*looper.The main looper for your application is c

12、reated by the Android environment,25.*so you should never need to call this function yourself.26.*link#prepare()27.*/28.29.publicstaticfinalvoid prepareMainLooper()30.prepare();名师资料总结-精品资料欢迎下载-名师精心整理-第 3 页,共 29 页 -31.setMainLooper(myLooper();32.if (Process.supportsProcesses()33.myLooper().mQueue.mQu

13、itAllowed=false;34.35.36.37.privatesynchronizedstaticvoid setMainLooper(Looper looper)38.mMainLooper=looper;39.40.41./*42.*Return the Looper object associated with the current thread.Returns43.*null if the calling thread is not associated with a Looper.44.*/45.publicstaticfinal Looper myLooper()46.r

14、eturn(Looper)sThreadLocal.get();47.48.49.private Looper()50.mQueue=new MessageQueue();51.mRun=true;52.mThread=Thread.currentThread();53.54.55.56.函数 prepareMainLooper做的事情其实就是在线程中创建一个Looper 对象,这个 Looper 对象是存放在 sThreadLocal成员变量里面的,成员变量sThreadLocal的类型为 ThreadLocal,表示这是一个线程局部变量,即保证每一个调用了prepareMainLooper

15、函数的线程里面都有一个独立的Looper 对象。在线程是创建 Looper 对象的工作是由prepare 函数来完成的,而在创建Looper 对象的时候,会同时创建一个消息队列 MessageQueue,保存在Looper 的成员变量mQueue 中,后续消息就是存放在这个队列中去。消息队列在 Android 应用程序消息处理机制中最重要的组件,因此,我们看看它的创建过程,即它的构造函数的实现,实现frameworks/base/core/java/android/os/MessageQueue.java文件中:view plain1.publicclass MessageQueue 2.3.

16、4.privateint mPtr;/used by native code5.6.privatenativevoid nativeInit();7.8.MessageQueue()9.nativeInit();名师资料总结-精品资料欢迎下载-名师精心整理-第 4 页,共 29 页 -10.11.12.13.它的初始化工作都交给JNI 方法 nativeInit 来实现了,这个JNI 方法定义在frameworks/base/core/jni/android_os_MessageQueue.cpp文件中:view plain1.staticvoid android_os_MessageQueu

17、e_nativeInit(JNIEnv*env,jobject obj)2.NativeMessageQueue*nativeMessageQueue=new NativeMessageQueue();3.if (!nativeMessageQueue)4.jniThrowRuntimeException(env,Unable to allocate native queue);5.return;6.7.8.android_os_MessageQueue_setNativeMessageQueue(env,obj,nativeMessageQueue);9.在 JNI 中,也相应地创建了一个消

18、息队列NativeMessageQueue,NativeMessageQueue类也是定义在 frameworks/base/core/jni/android_os_MessageQueue.cpp文件中,它的创建过程如下所示:view plain1.NativeMessageQueue:NativeMessageQueue()2.mLooper=Looper:getForThread();3.if (mLooper=NULL)4.mLooper=new Looper(false);5.Looper:setForThread(mLooper);6.7.它主要就是在内部创建了一个Looper 对

19、象,注意,这个Looper 对象是实现在JNI 层的,它与上面Java 层中的 Looper 是不一样的,不过它们是对应的,下面我们进一步分析消息循环的过程的时候,读者就会清楚地了解到它们之间的关系。这个 Looper 的创建过程也很重要,不过我们暂时放一放,先分析完android_os_MessageQueue_nativeInit函数的执行,它创建了本地消息队列NativeMessageQueue对象之后,接着调用android_os_MessageQueue_setNativeMessageQueue函数来把这个消息队列对象保存在前面我们在Java 层中创建的MessageQueue对象

20、的 mPtr 成员变量里面:view plain1.staticvoid android_os_MessageQueue_setNativeMessageQueue(JNIEnv*env,jobject messageQueueObj,2.NativeMessageQueue*nativeMessageQueue)3.env-SetIntField(messageQueueObj,gMessageQueueClassInfo.mPtr,名师资料总结-精品资料欢迎下载-名师精心整理-第 5 页,共 29 页 -4.reinterpret_cast(nativeMessageQueue);5.这里

21、传进来的参数messageQueueObj即为我们前面在Java 层创建的消息队列对象,而gMessageQueueClassInfo.mPtr即表示在Java 类 MessageQueue中,其成员变量mPtr 的偏移量,通过这个偏移量,就可以把这个本地消息队列对象natvieMessageQueue保存在 Java 层创建的消息队列对象的 mPtr 成员变量中,这是为了后续我们调用Java 层的消息队列对象的其它成员函数进入到JNI 层时,能够方便地找回它在JNI 层所对应的消息队列对象。我们再回到NativeMessageQueue的构造函数中,看看JNI 层的 Looper 对象的创建

22、过程,即看看它的构造函数是如何实现的,这个Looper 类实现在 frameworks/base/libs/utils/Looper.cpp文件中:view plain1.Looper:Looper(bool allowNonCallbacks):2.mAllowNonCallbacks(allowNonCallbacks),3.mResponseIndex(0)4.int wakeFds2;5.int result=pipe(wakeFds);6.7.8.mWakeReadPipeFd=wakeFds0;9.mWakeWritePipeFd=wakeFds1;10.11.12.13.#ifd

23、ef LOOPER_USES_EPOLL14./Allocate the epoll instance and register the wake pipe.15.mEpollFd=epoll_create(EPOLL_SIZE_HINT);16.17.18.struct epoll_event eventItem;19.memset(&eventItem,0,sizeof(epoll_event);/zero out unused members of data field union20.eventItem.events=EPOLLIN;21.eventItem.data.fd=mWake

24、ReadPipeFd;22.result=epoll_ctl(mEpollFd,EPOLL_CTL_ADD,mWakeReadPipeFd,&eventItem);23.24.#else25.26.#endif27.28.29.名师资料总结-精品资料欢迎下载-名师精心整理-第 6 页,共 29 页 -这个构造函数做的事情非常重要,它跟我们后面要介绍的应用程序主线程在消息队列中没有消息时要进入等待状态以及当消息队列有消息时要把应用程序主线程唤醒的这两个知识点息息相关。它主要就是通过 pipe 系统调用来创建了一个管道了:view plain1.int wakeFds2;2.int result=

25、pipe(wakeFds);3.4.5.mWakeReadPipeFd=wakeFds0;6.mWakeWritePipeFd=wakeFds1;管道是 Linux 系统中的一种进程间通信机制,具体可以参考前面一篇文章Android 学习启动篇 推荐的一本书 Linux 内核源代码情景分析中的第6 章-传统的 Uinx 进程间通信。简单来说,管道就是一个文件,在管道的两端,分别是两个打开文件文件描述符,这两个打开文件描述符都是对应同一个文件,其中一个是用来读的,别一个是用来写的,一般的使用方式就是,一个线程通过读文件描述符中来读管道的内容,当管道没有内容时,这个线程就会进入等待状态,而另外一个

26、线程通过写文件描述符来向管道中写入内容,写入内容的时候,如果另一端正有线程正在等待管道中的内容,那么这个线程就会被唤醒。这个等待和唤醒的操作是如何进行的呢,这就要借助Linux 系统中的 epoll 机制了。Linux 系统中的 epoll 机制为处理大批量句柄而作了改进的poll,是 Linux 下多路复用IO 接口 select/poll 的增强版本,它能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU 利用率。但是这里我们其实只需要监控的IO 接口只有 mWakeReadPipeFd一个,即前面我们所创建的管道的读端,为什么还需要用到epoll 呢?有点用牛刀来杀鸡的味道。其

27、实不然,这个 Looper 类是非常强大的,它除了监控内部所创建的管道接口之外,还提供了 addFd 接口供外界面调用,外界可以通过这个接口把自己想要监控的IO 事件一并加入到这个Looper对象中去,当所有这些被监控的IO 接口上面有事件发生时,就会唤醒相应的线程来处理,不过这里我们只关心刚才所创建的管道的IO 事件的发生。要使用 Linux 系统的 epoll 机制,首先要通过epoll_create来创建一个epoll 专用的文件描述符:view plain1.mEpollFd=epoll_create(EPOLL_SIZE_HINT);传入的参数EPOLL_SIZE_HINT是在这个

28、mEpollFd 上能监控的最大文件描述符数。接着还要通过epoll_ctl 函数来告诉epoll 要监控相应的文件描述符的什么事件:view plain1.struct epoll_event eventItem;2.memset(&eventItem,0,sizeof(epoll_event);/zero out unused members of data field union3.eventItem.events=EPOLLIN;4.eventItem.data.fd=mWakeReadPipeFd;5.result=epoll_ctl(mEpollFd,EPOLL_CTL_ADD,m

29、WakeReadPipeFd,&eventItem);名师资料总结-精品资料欢迎下载-名师精心整理-第 7 页,共 29 页 -这里就是告诉mEpollFd,它要监控 mWakeReadPipeFd文件描述符的EPOLLIN事件,即当管道中有内容可读时,就唤醒当前正在等待管道中的内容的线程。C+层的这个 Looper 对象创建好了之后,就返回到 JNI 层的 NativeMessageQueue的构造函数,最后就返回到 Java 层的消息队列MessageQueue的创建过程,这样,Java 层的 Looper 对象就准备好了。有点复杂,我们先小结一下这一步都做了些什么事情:A.在 Java

30、层,创建了一个Looper 对象,这个 Looper 对象是用来进入消息循环的,它的内部有一个消息队列 MessageQueue对象 mQueue;B.在 JNI 层,创建了一个NativeMessageQueue对象,这个 NativeMessageQueue对象保存在Java层的消息队列对象mQueue 的成员变量mPtr 中;C.在 C+层,创建了一个Looper 对象,保存在JNI 层的 NativeMessageQueue对象的成员变量mLooper 中,这个对象的作用是,当Java 层的消息队列中没有消息时,就使Android 应用程序主线程进入等待状态,而当 Java 层的消息队

31、列中来了新的消息后,就唤醒 Android 应用程序的主线程来处理这个消息。回到 ActivityThread类的 main 函数中,在上面这些工作都准备好之后,就调用Looper 类的 loop 函数进入到消息循环中去了:view plain1.publicclass Looper 2.3.4.publicstatic final void loop()5.Looper me=myLooper();6.MessageQueue queue=me.mQueue;7.8.9.10.while (true)11.Message msg=queue.next();/might block12.13.

32、14.if (msg!=null)15.if (msg.target=null)16./No target is a magic identifier for the quit message.17.return;18.19.20.21.22.msg.target.dispatchMessage(msg);23.名师资料总结-精品资料欢迎下载-名师精心整理-第 8 页,共 29 页 -24.25.26.msg.recycle();27.28.29.30.31.32.这里就是进入到消息循环中去了,它不断地从消息队列mQueue 中去获取下一个要处理的消息msg,如果消息的 target 成员变量

33、为null,就表示要退出消息循环了,否则的话就要调用这个target 对象的dispatchMessage成员函数来处理这个消息,这个target 对象的类型为Handler,下面我们分析消息的发送时会看到这个消息对象msg 是如设置的。这个函数最关键的地方便是从消息队列中获取下一个要处理的消息了,即 MessageQueue.next函数,它实现 frameworks/base/core/java/android/os/MessageQueue.java文件中:view plain1.publicclass MessageQueue 2.3.4.final Message next()5.i

34、nt pendingIdleHandlerCount=-1;/-1 only during first iteration6.int nextPollTimeoutMillis=0;7.8.for (;)9.if (nextPollTimeoutMillis!=0)10.Binder.flushPendingCommands();11.12.nativePollOnce(mPtr,nextPollTimeoutMillis);13.14.synchronized(this)15./Try to retrieve the next message.Return if found.16.final

35、long now=SystemClock.uptimeMillis();17.final Message msg=mMessages;18.if (msg!=null)19.finallong when=msg.when;20.if (now=when)21.mBlocked=false;22.mMessages=msg.next;23.msg.next=null;24.if (Config.LOGV)Log.v(MessageQueue,Returning message:+msg);25.return msg;名师资料总结-精品资料欢迎下载-名师精心整理-第 9 页,共 29 页 -26.

36、else 27.nextPollTimeoutMillis=(int)Math.min(when-now,Integer.MAX_VALUE);28.29.else 30.nextPollTimeoutMillis=-1;31.32.33./If first time,then get the number of idlers to run.34.if (pendingIdleHandlerCount 0)35.pendingIdleHandlerCount=mIdleHandlers.size();36.37.if (pendingIdleHandlerCount=0)38./No idle

37、 handlers to run.Loop and wait some more.39.mBlocked=true;40.continue;41.42.43.if (mPendingIdleHandlers=null)44.mPendingIdleHandlers=new IdleHandlerMath.max(pendingIdleHandlerCount,4);45.46.mPendingIdleHandlers=mIdleHandlers.toArray(mPendingIdleHandlers);47.48.49./Run the idle handlers.50./We only e

38、ver reach this code block during the first iteration.51.for (int i=0;i=when)5.mBlocked=false;6.mMessages=msg.next;7.msg.next=null;8.if (Config.LOGV)Log.v(MessageQueue,Returning message:+msg);9.return msg;10.else 11.nextPollTimeoutMillis=(int)Math.min(when-now,Integer.MAX_VALUE);12.13.else 14.nextPol

39、lTimeoutMillis=-1;名师资料总结-精品资料欢迎下载-名师精心整理-第 11 页,共 29 页 -15.如果消息队列中有消息,并且当前时候大于等于消息中的执行时间,那么就直接返回这个消息给Looper.loop消息处理,否则的话就要等待到消息的执行时间:view plain1.nextPollTimeoutMillis=(int)Math.min(when-now,Integer.MAX_VALUE);如果消息队列中没有消息,那就要进入无穷等待状态直到有新消息了:view plain1.nextPollTimeoutMillis=-1;-1 表示下次调用nativePollOnc

40、e时,如果消息中没有消息,就进入无限等待状态中去。这里计算出来的等待时间都是在下次调用nativePollOnce时使用的。这里说的等待,是空闲等待,而不是忙等待,因此,在进入空闲等待状态前,如果应用程序注册了IdleHandler接口来处理一些事情,那么就会先执行这里IdleHandler,然后再进入等待状态。IdlerHandler是定义在 MessageQueue的一个内部类:view plain1.publicclass MessageQueue 2.3.4./*5.*Callback interface for discovering when a thread is going t

41、o block6.*waiting for more messages.7.*/8.publicstaticinterface IdleHandler 9./*10.*Called when the message queue has run out of messages and will now11.*wait for more.Return true to keep your idle handler active,false12.*to have it removed.This may be called if there are still messages13.*pending i

42、n the queue,but they are all scheduled to be dispatched14.*after the current time.15.*/16.boolean queueIdle();17.18.19.20.名师资料总结-精品资料欢迎下载-名师精心整理-第 12 页,共 29 页 -它只有一个成员函数queueIdle,执行这个函数时,如果返回值为false,那么就会从应用程序中移除这个 IdleHandler,否则的话就会在应用程序中继续维护着这个IdleHandler,下次空闲时仍会再执会这个 IdleHandler。MessageQueue提供了 ad

43、dIdleHandler和 removeIdleHandler两注册和删除IdleHandler。回到 MessageQueue函数中,它接下来就是在进入等待状态前,看看有没有IdleHandler是需要执行的:view plain1./If first time,then get the number of idlers to run.2.if (pendingIdleHandlerCount 0)3.pendingIdleHandlerCount=mIdleHandlers.size();4.5.if (pendingIdleHandlerCount=0)6./No idle handle

44、rs to run.Loop and wait some more.7.mBlocked=true;8.continue;9.10.11.if (mPendingIdleHandlers=null)12.mPendingIdleHandlers=new IdleHandlerMath.max(pendingIdleHandlerCount,4);13.14.mPendingIdleHandlers=mIdleHandlers.toArray(mPendingIdleHandlers);如果没有,即pendingIdleHandlerCount等于 0,那下面的逻辑就不执行了,通过continu

45、e 语句直接进入下一次循环,否则就要把注册在mIdleHandlers中的 IdleHandler取出来,放在mPendingIdleHandlers数组中去。接下来就是执行这些注册了的IdleHanlder了:view plain1./Run the idle handlers.2./We only ever reach this code block during the first iteration.3.for (int i=0;i pendingIdleHandlerCount;i+)4.final IdleHandler idler=mPendingIdleHandlersi;5.

46、mPendingIdleHandlersi=null;/release the reference to the handler6.7.boolean keep=false;8.try 9.keep=idler.queueIdle();10.catch (Throwable t)11.Log.wtf(MessageQueue,IdleHandler threw exception,t);12.13.名师资料总结-精品资料欢迎下载-名师精心整理-第 13 页,共 29 页 -14.if(!keep)15.synchronized(this)16.mIdleHandlers.remove(idle

47、r);17.18.19.执行完这些IdleHandler之后,线程下次调用nativePollOnce函数时,就不设置超时时间了,因为,很有可能在执行IdleHandler的时候,已经有新的消息加入到消息队列中去了,因此,要重置nextPollTimeoutMillis的值:view plain1./While calling an idle handler,a new message could have been delivered2./so go back and look again for a pending message without waiting.3.nextPollTim

48、eoutMillis=0;分析完 MessageQueue的这个 next 函数之后,我们就要深入分析一下JNI 方法 nativePollOnce了,看看它是如何进入等待状态的,这个函数定义在frameworks/base/core/jni/android_os_MessageQueue.cpp文件中:view plain1.staticvoid android_os_MessageQueue_nativePollOnce(JNIEnv*env,jobject obj,2.jint ptr,jint timeoutMillis)3.NativeMessageQueue*nativeMessa

49、geQueue=reinterpret_cast(ptr);4.nativeMessageQueue-pollOnce(timeoutMillis);5.这个函数首先是通过传进入的参数ptr 取回前面在Java 层创建 MessageQueue对象时在JNI 层创建的 NatvieMessageQueue对象,然后调用它的pollOnce 函数:view plain1.void NativeMessageQueue:pollOnce(int timeoutMillis)2.mLooper-pollOnce(timeoutMillis);3.这里将操作转发给mLooper 对象的 pollOnc

50、e 函数处理,这里的mLooper对象是在 C+层的对象,它也是在前面在JNI 层创建的 NatvieMessageQueue对象时创建的,它的pollOnce函数定义在frameworks/base/libs/utils/Looper.cpp文件中:view plain1.int Looper:pollOnce(int timeoutMillis,int*outFd,int*outEvents,void*outData)2.int result=0;3.for (;)4.5.名师资料总结-精品资料欢迎下载-名师精心整理-第 14 页,共 29 页 -6.if (result!=0)7.8.9

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 教育专区 > 高考资料

本站为文档C TO C交易模式,本站只提供存储空间、用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。本站仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知得利文库网,我们立即给予删除!客服QQ:136780468 微信:18945177775 电话:18904686070

工信部备案号:黑ICP备15003705号-8 |  经营许可证:黑B2-20190332号 |   黑公网安备:91230400333293403D

© 2020-2023 www.deliwenku.com 得利文库. All Rights Reserved 黑龙江转换宝科技有限公司 

黑龙江省互联网违法和不良信息举报
举报电话:0468-3380021 邮箱:hgswwxb@163.com