Service的工作过程

Service分为两种工作状态,一种是启动状态,主要用于执行后台计算;另一种是绑定状态,主要用于其他组件和Service的交互。这两种状态可以共存的,即Service既可以处于启动状态也可以同时处于绑定状态。
本篇博客是对Service的启动过程和绑定过程进行源码分析。

介绍一下几个重要的概念:
1. Context:http://blog.csdn.net/qinjuning/article/details/7310620
2. ActivityThread、ApplicationThread:http://blog.csdn.net/myarrow/article/details/14223493

Service的启动过程

从ContextWrapper的startService开始:

@Overridepublic ComponentName startService(Intent service) {return mBase.startService(service);}

mBase的类型是ContextImpl。

@Overridepublic ComponentName startService(Intent service) {warnIfCallingFromSystemProcess();return startServiceCommon(service, mUser);}private ComponentName startServiceCommon(Intent service, UserHandle user) {try {validateServiceIntent(service);service.prepareToLeaveProcess();ComponentName cn = ActivityManagerNative.getDefault().startService(mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(getContentResolver()), getOpPackageName(), user.getIdentifier());if (cn != null) {if (cn.getPackageName().equals("!")) {throw new SecurityException("Not allowed to start service " + service+ " without permission " + cn.getClassName());} else if (cn.getPackageName().equals("!!")) {throw new SecurityException("Unable to start service " + service+ ": " + cn.getClassName());}}return cn;} catch (RemoteException e) {throw new RuntimeException("Failure from system", e);}}

由上可知,startServiceCommon通过ActivityManagerNative.getDefault()(AMS)这个对象启动一个Service。通过AMS来启动服务的行为是一个远程调用过程。
AMS#startService

@Overridepublic ComponentName startService(IApplicationThread caller, Intent service,String resolvedType, String callingPackage, int userId)throws TransactionTooLargeException {enforceNotIsolatedCaller("startService");// Refuse possible leaked file descriptorsif (service != null && service.hasFileDescriptors() == true) {throw new IllegalArgumentException("File descriptors passed in Intent");}if (callingPackage == null) {throw new IllegalArgumentException("callingPackage cannot be null");}if (DEBUG_SERVICE) Slog.v(TAG_SERVICE,"startService: " + service + " type=" + resolvedType);synchronized(this) {final int callingPid = Binder.getCallingPid();final int callingUid = Binder.getCallingUid();final long origId = Binder.clearCallingIdentity();ComponentName res = mServices.startServiceLocked(caller, service,resolvedType, callingPid, callingUid, callingPackage, userId);Binder.restoreCallingIdentity(origId);return res;}}

AMS会通过mService来完成Service后续的启动过程,mService对象的类型是ActivityServices,是一个辅助AMS进行Service管理的类,包括Service的启动、绑定和停止等。在ActivityServices的startServiceLocked方法会调用startServiceInnerLocked方法,startServiceInnerLocked方法继续调用bringUpServiceLocked方法,bringUpServiceLocked方法又调用realStartServiceLocked方法。

private final void realStartServiceLocked(ServiceRecord r,ProcessRecord app, boolean execInFg) throws RemoteException {if (app.thread == null) {throw new RemoteException();}if (DEBUG_MU)Slog.v(TAG_MU, "realStartServiceLocked, ServiceRecord.uid = " + r.appInfo.uid+ ", ProcessRecord.uid = " + app.uid);r.app = app;r.restartTime = r.lastActivity = SystemClock.uptimeMillis();final boolean newService = app.services.add(r);bumpServiceExecutingLocked(r, execInFg, "create");mAm.updateLruProcessLocked(app, false, null);mAm.updateOomAdjLocked();boolean created = false;try {if (LOG_SERVICE_START_STOP) {String nameTerm;int lastPeriod = r.shortName.lastIndexOf('.');nameTerm = lastPeriod >= 0 ? r.shortName.substring(lastPeriod) : r.shortName;EventLogTags.writeAmCreateService(r.userId, System.identityHashCode(r), nameTerm, r.app.uid, r.app.pid);}synchronized (r.stats.getBatteryStats()) {r.stats.startLaunchedLocked();}mAm.ensurePackageDexOpt(r.serviceInfo.packageName);app.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_SERVICE);app.thread.scheduleCreateService(r, r.serviceInfo,mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),app.repProcState);r.postNotification();created = true;} catch (DeadObjectException e) {Slog.w(TAG, "Application dead when creating service " + r);mAm.appDiedLocked(app);throw e;} finally {if (!created) {// Keep the executeNesting count accurate.final boolean inDestroying = mDestroyingServices.contains(r);serviceDoneExecutingLocked(r, inDestroying, inDestroying);// Cleanup.if (newService) {app.services.remove(r);r.app = null;}// Retry.if (!inDestroying) {scheduleServiceRestartLocked(r, false);}}}requestServiceBindingsLocked(r, execInFg);updateServiceClientActivitiesLocked(app, null, true);// If the service is in the started state, and there are no// pending arguments, then fake up one so its onStartCommand() will// be called.if (r.startRequested && r.callStart && r.pendingStarts.size() == 0) {r.pendingStarts.add(new ServiceRecord.StartItem(r, false, r.makeNextStartId(),null, null));}sendServiceArgsLocked(r, execInFg, true);if (r.delayed) {if (DEBUG_DELAYED_STARTS) Slog.v(TAG_SERVICE, "REM FR DELAY LIST (new proc): " + r);getServiceMap(r.userId).mDelayedStartList.remove(r);r.delayed = false;}if (r.delayedStop) {// Oh and hey we've already been asked to stop!r.delayedStop = false;if (r.startRequested) {if (DEBUG_DELAYED_STARTS) Slog.v(TAG_SERVICE,"Applying delayed stop (from start): " + r);stopServiceLocked(r);}}}

在realStartServiceLocked方法中,首先通过app.thread的scheduleCreateService方法来创建Service对象并调用其onCreate,接着调用sendServiceArgsLocked方法来调用Service的其他方法,比如onStartCommand,这两个过程均是进程间的通信。具体实现是ApplicationThread。
ApplicationThread#scheduleCreateService

        public final void scheduleCreateService(IBinder token,ServiceInfo info, CompatibilityInfo compatInfo, int processState) {updateProcessState(processState, false);CreateServiceData s = new CreateServiceData();s.token = token;s.info = info;s.compatInfo = compatInfo;sendMessage(H.CREATE_SERVICE, s);}

这个过程和Activity的启动类似。发送消息给Handler H,H接受消息H.CREATE_SERVICE并通过ActivityThread的handleCreateService方法来完成Service的启动。
ActivityThread#handleCreateService

    private void handleCreateService(CreateServiceData data) {// If we are getting ready to gc after going to the background, well// we are back active so skip it.unscheduleGcIdler();LoadedApk packageInfo = getPackageInfoNoCheck(data.info.applicationInfo, data.compatInfo);Service service = null;try {java.lang.ClassLoader cl = packageInfo.getClassLoader();service = (Service) cl.loadClass(data.info.name).newInstance();} catch (Exception e) {if (!mInstrumentation.onException(service, e)) {throw new RuntimeException("Unable to instantiate service " + data.info.name+ ": " + e.toString(), e);}}try {if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name);ContextImpl context = ContextImpl.createAppContext(this, packageInfo);context.setOuterContext(service);Application app = packageInfo.makeApplication(false, mInstrumentation);service.attach(context, this, data.info.name, data.token, app,ActivityManagerNative.getDefault());service.onCreate();mServices.put(data.token, service);try {ActivityManagerNative.getDefault().serviceDoneExecuting(data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);} catch (RemoteException e) {// nothing to do.}} catch (Exception e) {if (!mInstrumentation.onException(service, e)) {throw new RuntimeException("Unable to create service " + data.info.name+ ": " + e.toString(), e);}}}

主要完成了以下四件事:
1. 首先通过类加载器创建Service的实例
2. 然后创建Appliction对象并调用其onCreate
3. 接着调用ContextImpl对象并通过Service的attach方法建立二者的关系,和Activity的类似。
4. 最后调用Service的onCreate方法并将Service对象存储到ActivityThread中的一个列表中。
由于Service的onCreate被执行,意味着Service已经启动,除此之外,ActivityThread中还会通过handleServiceArgs方法调用Service的onStartCommand方法。

private void handleServiceArgs(ServiceArgsData data) {Service s = mServices.get(data.token);if (s != null) {try {if (data.args != null) {data.args.setExtrasClassLoader(s.getClassLoader());data.args.prepareToEnterProcess();}int res;if (!data.taskRemoved) {res = s.onStartCommand(data.args, data.flags, data.startId);} else {s.onTaskRemoved(data.args);res = Service.START_TASK_REMOVED_COMPLETE;}QueuedWork.waitToFinish();try {ActivityManagerNative.getDefault().serviceDoneExecuting(data.token, SERVICE_DONE_EXECUTING_START, data.startId, res);} catch (RemoteException e) {// nothing to do.}ensureJitEnabled();} catch (Exception e) {if (!mInstrumentation.onException(s, e)) {throw new RuntimeException("Unable to start service " + s+ " with " + data.args + ": " + e.toString(), e);}}}}

Service的绑定过程

Service的绑定过程也是从ContextWrapper开始。

@Overridepublic boolean bindService(Intent service, ServiceConnection conn,int flags) {return mBase.bindService(service, conn, flags);}

ContextImpl#bindServiceCommon

private boolean bindServiceCommon(Intent service, ServiceConnection conn, int flags,UserHandle user) {IServiceConnection sd;if (conn == null) {throw new IllegalArgumentException("connection is null");}if (mPackageInfo != null) {sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(),mMainThread.getHandler(), flags);} else {throw new RuntimeException("Not supported in system context");}validateServiceIntent(service);try {IBinder token = getActivityToken();if (token == null && (flags&BIND_AUTO_CREATE) == 0 && mPackageInfo != null&& mPackageInfo.getApplicationInfo().targetSdkVersion< android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {flags |= BIND_WAIVE_PRIORITY;}service.prepareToLeaveProcess();int res = ActivityManagerNative.getDefault().bindService(mMainThread.getApplicationThread(), getActivityToken(), service,service.resolveTypeIfNeeded(getContentResolver()),sd, flags, getOpPackageName(), user.getIdentifier());if (res < 0) {throw new SecurityException("Not allowed to bind to service " + service);}return res != 0;} catch (RemoteException e) {throw new RuntimeException("Failure from system", e);}}

首先将客户端的ServiceConnection对象转化为ServiceDispather.InnerConnection对象。因为服务的绑定有可能是跨进程的,ServiceConnection必须借助Binder才能让远程服务端回调自己的方法,ServiceDispather.InnerConnection刚好充当了Binder。ServiceDispather起连接ServiceConnection和InnerConnection的作用。此过程由LoadedApk的getServiceDispather方法完成。
LoadedApk#getServiceDispather

public final IServiceConnection getServiceDispatcher(ServiceConnection c,Context context, Handler handler, int flags) {synchronized (mServices) {LoadedApk.ServiceDispatcher sd = null;ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> map = mServices.get(context);if (map != null) {sd = map.get(c);}if (sd == null) {sd = new ServiceDispatcher(c, context, handler, flags);if (map == null) {map = new ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>();mServices.put(context, map);}map.put(c, sd);} else {sd.validate(context, handler);}return sd.getIServiceConnection();}}

mService是一个ArrayMap存储了一个应用当前活动的ServiceConnection和ServiceDispatcher的映射关系。
系统首先会查找是否存在相同的ServiceConnection,若不存在,则重新创建一个ServiceDispatcher对象并将其存储在mService中,在ServiceDispatcher内部又保存了ServiceConnection和InnerConnection对象。当Service和客户端建立连接后,系统会通过InnerConnection来调用ServiceConnection中的onServiceConnected方法,这个过程可能跨进程。当ServiceDispatcher创建好以后,getServiceDispatcher会返回其保存的InnerConnection对象。
AMS#bindService

public int bindService(IApplicationThread caller, IBinder token, Intent service,String resolvedType, IServiceConnection connection, int flags, String callingPackage,int userId) throws TransactionTooLargeException {enforceNotIsolatedCaller("bindService");// Refuse possible leaked file descriptorsif (service != null && service.hasFileDescriptors() == true) {throw new IllegalArgumentException("File descriptors passed in Intent");}if (callingPackage == null) {throw new IllegalArgumentException("callingPackage cannot be null");}synchronized(this) {return mServices.bindServiceLocked(caller, token, service,resolvedType, connection, flags, callingPackage, userId);}}

接下来AMS会调用ActivityServices的startServiceLocked方法。在ActivityServices的startServiceLocked方法会调用startServiceInnerLocked方法,startServiceInnerLocked方法继续调用bringUpServiceLocked方法,bringUpServiceLocked方法又调用realStartServiceLocked方法。最终通过ApplicationThread来完成Service实例的创建并执行onCreate方法,和启动Service不同的是,Service的绑定过程会调用app.thread的scheduleBindService方法。

public final void scheduleBindService(IBinder token, Intent intent,boolean rebind, int processState) {updateProcessState(processState, false);BindServiceData s = new BindServiceData();s.token = token;s.intent = intent;s.rebind = rebind;if (DEBUG_SERVICE)Slog.v(TAG, "scheduleBindService token=" + token + " intent=" + intent + " uid="+ Binder.getCallingUid() + " pid=" + Binder.getCallingPid());sendMessage(H.BIND_SERVICE, s);}

在H内部收到BIND_SERVICE,会交给ActivityThread的handleBindService方法来处理。
ActivityThread#handleBindService

private void handleBindService(BindServiceData data) {Service s = mServices.get(data.token);if (DEBUG_SERVICE)Slog.v(TAG, "handleBindService s=" + s + " rebind=" + data.rebind);if (s != null) {try {data.intent.setExtrasClassLoader(s.getClassLoader());data.intent.prepareToEnterProcess();try {if (!data.rebind) {IBinder binder = s.onBind(data.intent);ActivityManagerNative.getDefault().publishService(data.token, data.intent, binder);} else {s.onRebind(data.intent);ActivityManagerNative.getDefault().serviceDoneExecuting(data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);}ensureJitEnabled();} catch (RemoteException ex) {}} catch (Exception e) {if (!mInstrumentation.onException(s, e)) {throw new RuntimeException("Unable to bind to service " + s+ " with " + data.intent + ": " + e.toString(), e);}}}}

在handleBindService中,首先根据Service的token取出Service对象,然后调用Service的onBind方法,Service的onBind方法返回一个Binder对象给客户端使用。原则上来说,Service的onBind方法调用后Service就被绑定成功,但是onBind方法是Service的方法,这个时候客户端并不知道已经成功连接Service,所以还要调用客户端的ServiceConnection中的onServiceConnected,这是由AMS的publishService方法完成。
Service有一个特性,当多次绑定同一个Service时,Service的onBind方法只会执行一次,除非Service被终止。当Service的onBind执行以后,系统还需要告知客户端已经成功连接Service了。由AMS的publishService方法完成。
AMS#publishService

public void publishService(IBinder token, Intent intent, IBinder service) {// Refuse possible leaked file descriptorsif (intent != null && intent.hasFileDescriptors() == true) {throw new IllegalArgumentException("File descriptors passed in Intent");}synchronized(this) {if (!(token instanceof ServiceRecord)) {throw new IllegalArgumentException("Invalid service token");}mServices.publishServiceLocked((ServiceRecord)token, intent, service);}}

由上可知,AMS的publishService方法将具体的工作交给了ActiveServices类型的mService对象来处理。

     void publishServiceLocked(ServiceRecord r, Intent intent, IBinder service) {final long origId = Binder.clearCallingIdentity();try {if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "PUBLISHING " + r+ " " + intent + ": " + service);if (r != null) {Intent.FilterComparison filter= new Intent.FilterComparison(intent);IntentBindRecord b = r.bindings.get(filter);if (b != null && !b.received) {b.binder = service;b.requested = true;b.received = true;for (int conni=r.connections.size()-1; conni>=0; conni--) {ArrayList<ConnectionRecord> clist = r.connections.valueAt(conni);for (int i=0; i<clist.size(); i++) {ConnectionRecord c = clist.get(i);if (!filter.equals(c.binding.intent.intent)) {if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Not publishing to: " + c);if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Bound intent: " + c.binding.intent.intent);if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Published intent: " + intent);continue;}if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Publishing to: " + c);try {c.conn.connected(r.name, service);} catch (Exception e) {Slog.w(TAG, "Failure sending service " + r.name +" to connection " + c.conn.asBinder() +" (in " + c.binding.client.processName + ")", e);}}}}serviceDoneExecutingLocked(r, mDestroyingServices.contains(r), false);}} finally {Binder.restoreCallingIdentity(origId);}

核心代码c.conn.connected(r.name, service); 其中c是ConnectionRecord;c.conn是ServiceDispatcher.InnerConnection,Service就是Service的onBind返回的Binder对象。
LoadedApk#ServiceDispatcher#InnerConnection

        private static class InnerConnection extends IServiceConnection.Stub {final WeakReference<LoadedApk.ServiceDispatcher> mDispatcher;InnerConnection(LoadedApk.ServiceDispatcher sd) {mDispatcher = new WeakReference<LoadedApk.ServiceDispatcher>(sd);}public void connected(ComponentName name, IBinder service) throws RemoteException {LoadedApk.ServiceDispatcher sd = mDispatcher.get();if (sd != null) {sd.connected(name, service);}}}

InnerConnection的connected方法调用ServiceDispatcher的connected方法。
LoadedApk#ServiceDispatcher#connected

        public void connected(ComponentName name, IBinder service) {if (mActivityThread != null) {mActivityThread.post(new RunConnection(name, service, 0));} else {doConnected(name, service);}}

ServiceDispatcher的mActivityThread是ActivityThread中的Handler H。如此,RunConnection就可以经由H的post方法从而运行在主线程中,因此客户端的ServiceConnection中的方法是在主线程被回调的。

        private final class RunConnection implements Runnable {RunConnection(ComponentName name, IBinder service, int command) {mName = name;mService = service;mCommand = command;}public void run() {if (mCommand == 0) {doConnected(mName, mService);} else if (mCommand == 1) {doDeath(mName, mService);}}final ComponentName mName;final IBinder mService;final int mCommand;}

显然,RunConnection的润方法也是简单的调用了ServiceDispatcher的doConnected方法,由于ServiceDispatcher内部保存了客户端的ServiceConnection对象,因此它可以很方便地调用ServiceConnection对象的onServiceConnected方法。
LoadedApk#ServiceDispatcher#doConnected

        public void doConnected(ComponentName name, IBinder service) {ServiceDispatcher.ConnectionInfo old;ServiceDispatcher.ConnectionInfo info;synchronized (this) {if (mForgotten) {// We unbound before receiving the connection; ignore// any connection received.return;}old = mActiveConnections.get(name);if (old != null && old.binder == service) {// Huh, already have this one.  Oh well!return;}if (service != null) {// A new service is being connected... set it all up.mDied = false;info = new ConnectionInfo();info.binder = service;info.deathMonitor = new DeathMonitor(name, service);try {service.linkToDeath(info.deathMonitor, 0);mActiveConnections.put(name, info);} catch (RemoteException e) {// This service was dead before we got it...  just// don't do anything with it.mActiveConnections.remove(name);return;}} else {// The named service is being disconnected... clean up.mActiveConnections.remove(name);}if (old != null) {old.binder.unlinkToDeath(old.deathMonitor, 0);}}// If there was an old service, it is not disconnected.if (old != null) {mConnection.onServiceDisconnected(name);}// If there is a new service, it is now connected.if (service != null) {mConnection.onServiceConnected(name, service);}}

客户端的onServiceConnected方法执行后,Service的绑定过程也就完成了。

Service的工作过程相关推荐

  1. 《Android开发艺术探索》第9章-四大组件的工作过程读书笔记

    目录 1 四大组件的运行状态 2 Activity 的工作过程 2.1 Activity 的启动过程 3 Service 的工作过程 3.1 Service 有哪两种工作状态?这两种状态可以共存吗? ...

  2. 框架源码系列四:手写Spring-配置(为什么要提供配置的方法、选择什么样的配置方式、配置方式的工作过程是怎样的、分步骤一个一个的去分析和设计)...

    一.为什么要提供配置的方法 经过前面的手写Spring IOC.手写Spring DI.手写Spring AOP,我们知道要创建一个bean对象,需要用户先定义好bean,然后注册到bean工厂才能创 ...

  3. 4.四大组件的工作过程

    四大组件的工作过程 这篇文章只是对刘望舒大佬书籍的摘抄,适合复习用,没看过的请先看大佬的原书 下面是大佬博客的链接Android进阶三部曲 第二部<Android进阶解密> 一.根Acti ...

  4. android进阶(九)-----Android四大组件的工作过程

    一.四大组件的运行状态 android四大组件中国除了BroadcastReceiver以外,其他三种都必须在AndroidManifest中注册,对于BroadcastReceiver既可以在And ...

  5. Android开发艺术探索 - 第9章 四大组件的工作过程

    1.Activity启动过程 ref 从Activity的startActivity方法开始.startActivity的多个重载方法,最终都会调用startActivityForResult方法.m ...

  6. Android开发艺术探索——第九章:四大组件的工作过程(下)

    我們继续来看四大组件的工作过程 一.BroadcastReceiver的工作过程 广播的工作过程,我们主要看两个方面,一个是注册过程,另一个就是接收的过程,我们想使用广播是非常简单的,只需要继承Bro ...

  7. 【转】DHCP工作过程详解

    DHCP动态主机配置协议的作用我想作为网管的兄弟们都应该知道了,这里我就不多废话了,今天我要谈的是DHCP的工作过程,了解了工作过程,要排除故障就容易了. 一.DHCP客户机初始化: 1. 寻找DHC ...

  8. 实验四:汇编代码调用系统调用的工作过程

    钟晶晶 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 工作过程 以41 ...

  9. [置顶] 谈谈找工作过程中的那些环节、注意点和经验

    作者:寒小阳 时间:2013年9月. 出处:http://blog.csdn.net/han_xiaoyang/. 声明:版权所有,转载请注明出处,谢谢. 前面写了两篇面试相关的总结性文章,一篇是找工 ...

最新文章

  1. javaScript由哪些部分组成?
  2. 【人物】徐磊:对用户驯养,只需要让用户记得你会给肉
  3. 物联网平台存量设备如何一键迁移企业版实例
  4. orcle抽数据到mysql_抽取oracle数据到mysql数据库的实现过程
  5. 深入理解.net服务器控件
  6. Go语言从入门到精通 - 数据类型转换
  7. qt中创建控件布局以及删除原有布局和控件
  8. 【剑指Offer学习】【面试题22:栈的压入、弹出序列】
  9. 图形学基础|环境光遮蔽(Ambient Occlusion)
  10. 一般的java项目后台都有什么技术?
  11. 高等教育学备考:教育学概述
  12. QWidget setStyleSheet无效
  13. 流程引擎之compileflow简介
  14. Windows 当前所接的屏幕检测
  15. modbus tcp主站和从站_Modbus-RTU 一主多从PLC无线通讯经典案例
  16. 网易校园招聘java面试经历
  17. 巧用“谷歌学术”,轻松完成参考文献
  18. 少儿编程行业前景分析
  19. JavaWeb基于老杜课程笔记的完善
  20. PCIe 资料收集2

热门文章

  1. 2021.11.27月赛题解
  2. 2021-2027全球与中国核磁共振管市场现状及未来发展趋势
  3. java上传微博图床_php上传图片到微博图床
  4. 【ETH链游】阿蟹Axie Infinity模拟器运行及多开
  5. Ambari2.7.4配置HIVE_AUX_JARS_PATH
  6. 欲善其事,先利其器——青龙面板依赖安装教程
  7. 联盟平台开屏广告收入大减,APP开发者应如何应对?
  8. com调用excel后,进程未关闭解决方案
  9. 2019年浙江大学计算机考研复试线,2019年浙江大学考研复试分数线已经公布
  10. numpy矩阵与向量类型的向量乘法