当前位置: 首页 > news >正文

Android中Service在新进程中的启动流程2

       

目录

1、Service在客户端的启动入口

2、Service启动在AMS的处理

3、Service在新进程中的启动

4、Service与AMS的关系再续


        上一篇文章中我们了解了Service在新进程中启动的大致流程,同时认识了与客户端进程交互的接口IApplicationThread以及与AMS交互的接口IActivityManager,客户端进程与AMS则借助于Binder驱动实现了二者之间的通信,这一篇文章我们将深入源码分析startService是如何一步步启动新服务的,源码同样基于Android2.3.7。

        首先放上上一篇的流程概览图,方便源码分析。

1、Service在客户端的启动入口

        该步骤对应流程图中的第一步,通过调用Context.startService启动服务,该动作最终会走到ContextImpl的startService函数中,代码如下:

//frameworks\base\core\java\android\app\Contextlmpl.java@Overridepublic ComponentName startService(Intent service) {try {ComponentName cn = ActivityManagerNative.getDefault().startService(mMainThread.getApplicationThread(), service,service.resolveTypeIfNeeded(getContentResolver()));if (cn != null && cn.getPackageName().equals("!")) {throw new SecurityException("Not allowed to start service " + service+ " without permission " + cn.getClassName());}return cn;} catch (RemoteException e) {return null;}}

        通过调用ActivityManagerNative.getDefault()来启动服务,ActivityManagerNative.getDefault()返回的是AMS的一个代理对象ActivityManagerProxy,上一篇我们说过ActivityManagerProxy实现了IActivityManager接口,这里通过该代理把请求转发到AMS去,代码如下:

//frameworks\base\core\java\android\app\ActivityManagerNative.java
class ActivityManagerProxy implements IActivityManager
{public ActivityManagerProxy(IBinder remote){mRemote = remote;}public IBinder asBinder(){return mRemote;}//...//启动服务public ComponentName startService(IApplicationThread caller, Intent service,String resolvedType) throws RemoteException{Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeInterfaceToken(IActivityManager.descriptor);data.writeStrongBinder(caller != null ? caller.asBinder() : null);service.writeToParcel(data, 0);data.writeString(resolvedType);mRemote.transact(START_SERVICE_TRANSACTION, data, reply, 0);reply.readException();ComponentName res = ComponentName.readFromParcel(reply);data.recycle();reply.recycle();return res;}//停止服务public int stopService(IApplicationThread caller, Intent service,String resolvedType) throws RemoteException{Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeInterfaceToken(IActivityManager.descriptor);data.writeStrongBinder(caller != null ? caller.asBinder() : null);service.writeToParcel(data, 0);data.writeString(resolvedType);mRemote.transact(STOP_SERVICE_TRANSACTION, data, reply, 0);reply.readException();int res = reply.readInt();reply.recycle();data.recycle();return res;}public boolean stopServiceToken(ComponentName className, IBinder token,int startId) throws RemoteException {Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeInterfaceToken(IActivityManager.descriptor);ComponentName.writeToParcel(className, data);data.writeStrongBinder(token);data.writeInt(startId);mRemote.transact(STOP_SERVICE_TOKEN_TRANSACTION, data, reply, 0);reply.readException();boolean res = reply.readInt() != 0;data.recycle();reply.recycle();return res;}public void setServiceForeground(ComponentName className, IBinder token,int id, Notification notification, boolean removeNotification) throws RemoteException {Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeInterfaceToken(IActivityManager.descriptor);ComponentName.writeToParcel(className, data);data.writeStrongBinder(token);data.writeInt(id);if (notification != null) {data.writeInt(1);notification.writeToParcel(data, 0);} else {data.writeInt(0);}data.writeInt(removeNotification ? 1 : 0);mRemote.transact(SET_SERVICE_FOREGROUND_TRANSACTION, data, reply, 0);reply.readException();data.recycle();reply.recycle();}//绑定服务public int bindService(IApplicationThread caller, IBinder token,Intent service, String resolvedType, IServiceConnection connection,int flags) throws RemoteException {Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeInterfaceToken(IActivityManager.descriptor);data.writeStrongBinder(caller != null ? caller.asBinder() : null);data.writeStrongBinder(token);service.writeToParcel(data, 0);data.writeString(resolvedType);data.writeStrongBinder(connection.asBinder());data.writeInt(flags);mRemote.transact(BIND_SERVICE_TRANSACTION, data, reply, 0);reply.readException();int res = reply.readInt();data.recycle();reply.recycle();return res;}//解绑服务public boolean unbindService(IServiceConnection connection) throws RemoteException{Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeInterfaceToken(IActivityManager.descriptor);data.writeStrongBinder(connection.asBinder());mRemote.transact(UNBIND_SERVICE_TRANSACTION, data, reply, 0);reply.readException();boolean res = reply.readInt() != 0;data.recycle();reply.recycle();return res;}public void publishService(IBinder token,Intent intent, IBinder service) throws RemoteException {Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeInterfaceToken(IActivityManager.descriptor);data.writeStrongBinder(token);intent.writeToParcel(data, 0);data.writeStrongBinder(service);mRemote.transact(PUBLISH_SERVICE_TRANSACTION, data, reply, 0);reply.readException();data.recycle();reply.recycle();}    //...
}

         这里列举了与服务相关的操作,我们关注启动服务mRemote.transact(START_SERVICE_TRANSACTION, data, reply, 0),mRemote代表了远程AMS对象,它会把请求发送给Binder驱动,然后由Binder驱动负责把请求转发到AMS去,参数caller代表了源进程即发起请求的进程,service代表了我们要启动的服务。

        以上动作都发生在源进程,下面进入AMS服务侧的系统进程了。

        接收到startService动作的是ActivityManagerNative,根据标识START_SERVICE_TRANSACTION回调到以下代码:

//frameworks\base\core\java\android\app\ActivityManagerNative.java
public abstract class ActivityManagerNative extends Binder implements IActivityManager
{/*** Cast a Binder object into an activity manager interface, generating* a proxy if needed.*/static public IActivityManager asInterface(IBinder obj){if (obj == null) {return null;}IActivityManager in =(IActivityManager)obj.queryLocalInterface(descriptor);if (in != null) {return in;}return new ActivityManagerProxy(obj);}/*** Retrieve the system's default/global activity manager.*/static public IActivityManager getDefault(){if (gDefault != null) {//if (Config.LOGV) Log.v(//    "ActivityManager", "returning cur default = " + gDefault);return gDefault;}IBinder b = ServiceManager.getService("activity");if (Config.LOGV) Log.v("ActivityManager", "default service binder = " + b);gDefault = asInterface(b);if (Config.LOGV) Log.v("ActivityManager", "default service = " + gDefault);return gDefault;}//...public ActivityManagerNative(){attachInterface(this, descriptor);}public boolean onTransact(int code, Parcel data, Parcel reply, int flags)throws RemoteException {switch (code) {//...//启动服务case START_SERVICE_TRANSACTION: {data.enforceInterface(IActivityManager.descriptor);IBinder b = data.readStrongBinder();IApplicationThread app = ApplicationThreadNative.asInterface(b);Intent service = Intent.CREATOR.createFromParcel(data);String resolvedType = data.readString();ComponentName cn = startService(app, service, resolvedType);reply.writeNoException();ComponentName.writeToParcel(cn, reply);return true;}case STOP_SERVICE_TRANSACTION: {data.enforceInterface(IActivityManager.descriptor);IBinder b = data.readStrongBinder();IApplicationThread app = ApplicationThreadNative.asInterface(b);Intent service = Intent.CREATOR.createFromParcel(data);String resolvedType = data.readString();int res = stopService(app, service, resolvedType);reply.writeNoException();reply.writeInt(res);return true;}case STOP_SERVICE_TOKEN_TRANSACTION: {data.enforceInterface(IActivityManager.descriptor);ComponentName className = ComponentName.readFromParcel(data);IBinder token = data.readStrongBinder();int startId = data.readInt();boolean res = stopServiceToken(className, token, startId);reply.writeNoException();reply.writeInt(res ? 1 : 0);return true;}case SET_SERVICE_FOREGROUND_TRANSACTION: {data.enforceInterface(IActivityManager.descriptor);ComponentName className = ComponentName.readFromParcel(data);IBinder token = data.readStrongBinder();int id = data.readInt();Notification notification = null;if (data.readInt() != 0) {notification = Notification.CREATOR.createFromParcel(data);}boolean removeNotification = data.readInt() != 0;setServiceForeground(className, token, id, notification, removeNotification);reply.writeNoException();return true;}case BIND_SERVICE_TRANSACTION: {data.enforceInterface(IActivityManager.descriptor);IBinder b = data.readStrongBinder();IApplicationThread app = ApplicationThreadNative.asInterface(b);IBinder token = data.readStrongBinder();Intent service = Intent.CREATOR.createFromParcel(data);String resolvedType = data.readString();b = data.readStrongBinder();int fl = data.readInt();IServiceConnection conn = IServiceConnection.Stub.asInterface(b);int res = bindService(app, token, service, resolvedType, conn, fl);reply.writeNoException();reply.writeInt(res);return true;}case UNBIND_SERVICE_TRANSACTION: {data.enforceInterface(IActivityManager.descriptor);IBinder b = data.readStrongBinder();IServiceConnection conn = IServiceConnection.Stub.asInterface(b);boolean res = unbindService(conn);reply.writeNoException();reply.writeInt(res ? 1 : 0);return true;}case PUBLISH_SERVICE_TRANSACTION: {data.enforceInterface(IActivityManager.descriptor);IBinder token = data.readStrongBinder();Intent intent = Intent.CREATOR.createFromParcel(data);IBinder service = data.readStrongBinder();publishService(token, intent, service);reply.writeNoException();return true;}case UNBIND_FINISHED_TRANSACTION: {data.enforceInterface(IActivityManager.descriptor);IBinder token = data.readStrongBinder();Intent intent = Intent.CREATOR.createFromParcel(data);boolean doRebind = data.readInt() != 0;unbindFinished(token, intent, doRebind);reply.writeNoException();return true;}//...}
}

        这里关注START_SERVICE_TRANSACTION分支的处理, 从binder中读出了IApplicationThread对象,这个对象代表了发起请求的源进程,取出service,该Intent代表了我们要启动的服务。接着调用startService(app, service, resolvedType),该函数的实现正是位于AMS中,接下来我们进入AMS分析它是如何启动服务的。

2、Service启动在AMS的处理

        现在我们进入到AMS的处理中,继续调用分析startService函数调用,代码如下:

    //frameworks\base\servicesljava\com\android\server\am\ActivityManagerService.javapublic ComponentName startService(IApplicationThread caller, Intent service,String resolvedType) {// Refuse possible leaked file descriptorsif (service != null && service.hasFileDescriptors() == true) {throw new IllegalArgumentException("File descriptors passed in Intent");}synchronized(this) {final int callingPid = Binder.getCallingPid();final int callingUid = Binder.getCallingUid();final long origId = Binder.clearCallingIdentity();ComponentName res = startServiceLocked(caller, service,resolvedType, callingPid, callingUid);Binder.restoreCallingIdentity(origId);return res;}}

        直接转发到函数startServiceLocked中,代码如下:

    ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType, int callingPid, int callingUid) {synchronized(this) {//...ServiceLookupResult res =retrieveServiceLocked(service, resolvedType, callingPid, callingUid);if (res == null) {return null;}if (res.record == null) {return new ComponentName("!", res.permission != null? res.permission : "private to package");}ServiceRecord r = res.record;int targetPermissionUid = checkGrantUriPermissionFromIntentLocked(callingUid, r.packageName, service);if (unscheduleServiceRestartLocked(r)) {if (DEBUG_SERVICE) Slog.v(TAG, "START SERVICE WHILE RESTART PENDING: " + r);}r.startRequested = true;r.callStart = false;r.lastStartId++;if (r.lastStartId < 1) {r.lastStartId = 1;}r.pendingStarts.add(new ServiceRecord.StartItem(r, r.lastStartId,service, targetPermissionUid));r.lastActivity = SystemClock.uptimeMillis();synchronized (r.stats.getBatteryStats()) {r.stats.startRunningLocked();}if (!bringUpServiceLocked(r, service.getFlags(), false)) {return new ComponentName("!", "Service process is bad");}return r.name;}}

         首先通过调用retrieveServiceLocked拿到对应的ServiceLookupResult对象,该对象包含了ServiceRecord对象,ServiceRecord则是解析参数service拿到的。

        接下来设置ServiceRecord的几个参数startRequested = true、callStart = false、lastStartId++。接着创建ServiceRecord.StartItem对象并放到ServiceRecord的pendingStarts成员变量中,代表了接下来有需要启动的Service。接着继续调用bringUpServiceLocked启动服务,代码如下:

    private final boolean bringUpServiceLocked(ServiceRecord r,int intentFlags, boolean whileRestarting) {//1if (r.app != null && r.app.thread != null) {sendServiceArgsLocked(r, false);return true;}//2if (!whileRestarting && r.restartDelay > 0) {// If waiting for a restart, then do nothing.return true;}// We are now bringing the service up, so no longer in the// restarting state.mRestartingServices.remove(r);//3final String appName = r.processName;ProcessRecord app = getProcessRecordLocked(appName, r.appInfo.uid);if (app != null && app.thread != null) {try {realStartServiceLocked(r, app);return true;} catch (RemoteException e) {Slog.w(TAG, "Exception when starting service " + r.shortName, e);}// If a dead object exception was thrown -- fall through to// restart the application.}// Not running -- get it started, and enqueue this service record// to be executed when the app comes up.//4if (startProcessLocked(appName, r.appInfo, true, intentFlags,"service", r.name, false) == null) {bringDownServiceLocked(r, true);return false;}//5if (!mPendingServices.contains(r)) {mPendingServices.add(r);}return true;}

         函数参数中ServiceRecord就是我们要启动的Service对应的对象,whileRestarting为false,intentFlags则是与服务相关的一些参数。代码中我标记了5个地方,这里会一一说明:

  1. r.app != null && r.app.thread != null:这个代表了我们要启动的进程以及起来了,不需要再启动新进程了,所以接下来调用sendServiceArgsLocked执行服务的生命周期函数,函数返回。由于新进程还没有启动,所以不会进入该分支。
  2. !whileRestarting && r.restartDelay > 0:whileRestarting为false,但是restartDelay参数并没有设置。所以不会进入该分支。
  3. app != null && app.thread != null:代表新进程是否以及存在,明显不存在;所以也不会进入这里。
  4. startProcessLocked:由于新进程没有启动,所以调用startProcessLocked启动服务要运行在的进程。
  5. mPendingServices.add(r):把ServiceRecord对象加入到集合mPendingServices中,也就是说,当进程起来以后需要从集合mPendingServices中拿到需要运行的服务。

        第四点启动新进程会把需要运行服务的进程创建起来,接下来我们进入startProcessLocked,代码如下:

    final ProcessRecord startProcessLocked(String processName,ApplicationInfo info, boolean knownToBeDead, int intentFlags,String hostingType, ComponentName hostingName, boolean allowWhileBooting) {ProcessRecord app = getProcessRecordLocked(processName, info.uid);// We don't have to do anything more if:// (1) There is an existing application record; and// (2) The caller doesn't think it is dead, OR there is no thread//     object attached to it so we know it couldn't have crashed; and// (3) There is a pid assigned to it, so it is either starting or//     already running.//...if (app == null) {app = newProcessRecordLocked(null, info, processName);mProcessNames.put(processName, info.uid, app);} else {// If this is a new package in the process, add the package to the listapp.addPackage(info.packageName);}//...startProcessLocked(app, hostingType, hostingNameStr);return (app.pid != 0) ? app : null;}

        代码省略了一些相关的判断,如果需要创建新进程, 则会调用newProcessRecordLocked创建一个ProcessRecord对象用于记录新进程的一些信息。继续调用到startProcessLocked,代码如下:

    private final void startProcessLocked(ProcessRecord app,String hostingType, String hostingNameStr) {//...try {//1 设置一些启动参数int uid = app.info.uid;int[] gids = null;//...int debugFlags = 0;if ((app.info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {debugFlags |= Zygote.DEBUG_ENABLE_DEBUGGER;}// Run the app in safe mode if its manifest requests so or the// system is booted in safe mode.if ((app.info.flags & ApplicationInfo.FLAG_VM_SAFE_MODE) != 0 ||Zygote.systemInSafeMode == true) {debugFlags |= Zygote.DEBUG_ENABLE_SAFEMODE;}if ("1".equals(SystemProperties.get("debug.checkjni"))) {debugFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;}if ("1".equals(SystemProperties.get("debug.assert"))) {debugFlags |= Zygote.DEBUG_ENABLE_ASSERT;}//2 启动新进程,入口类为android.app.ActivityThreadint pid = Process.start("android.app.ActivityThread",mSimpleProcessManagement ? app.processName : null, uid, uid,gids, debugFlags, null);//...if (pid == 0 || pid == MY_PID) {// Processes are being emulated with threads.app.pid = MY_PID;app.removed = false;mStartingProcesses.add(app);} else if (pid > 0) {//3 发送了一个超时消息app.pid = pid;app.removed = false;synchronized (mPidsSelfLocked) {this.mPidsSelfLocked.put(pid, app);Message msg = mHandler.obtainMessage(PROC_START_TIMEOUT_MSG);msg.obj = app;mHandler.sendMessageDelayed(msg, PROC_START_TIMEOUT);}} else {app.pid = 0;//...}} catch (RuntimeException e) {// XXX do better error recovery.app.pid = 0;Slog.e(TAG, "Failure starting process " + app.processName, e);}}

        该函数中有三个关键位置,接下来我们一一说明:

  1. 这里主要设置一些新进程的启动参数,比如uid、gids、debugFlags。
  2. 启动新进程,入口类为android.app.ActivityThread
  3. 新进程创建成功返回了,获得了进程id,也即是返回值pid ;同时还把ProcessRecord对象放入到集合mPidsSelfLocked中,然后发送了一个超时消息PROC_START_TIMEOUT_MSG。
  • mPidsSelfLocked:是AMS用来记录当前启动进程信息的集合。
  • PROC_START_TIMEOUT_MSG:则是新进程创建成功后发送的一个延时消息,AMS这里采取的策略是新进程必须在启动之后的一段时间内回调到AMS,此时会把消息该消息移除掉;如果新进程启动太久了,没有及时回调AMS则该消息会弹出ANR弹窗提醒用户。言下之意就是新进程启动太慢了,影响了用户体验,所以通过这种方式告知用户,用户可以选择继续等待或者杀死待启动的应用。

        新进程启动的入口类为android.app.ActivityThread,首先调用的是ActivityThread.main,接下来运行流程就到了客户端的MyService进程中了。

有关新进程如何启动,可以再写一遍文章分析,这里关注Service的启动流程,先简单跳过了。

3、Service在新进程中的启动

        新进程启动后的入口为android.app.ActivityThread.main,代码如下:

    //frameworks\base\core\java\android\app\ActivityThread.javapublic static final void main(String[] args) {SamplingProfilerIntegration.start();Process.setArgV0("<pre-initialized>");Looper.prepareMainLooper();if (sMainThreadHandler == null) {sMainThreadHandler = new Handler();}ActivityThread thread = new ActivityThread();thread.attach(false);if (false) {Looper.myLooper().setMessageLogging(newLogPrinter(Log.DEBUG, "ActivityThread"));}Looper.loop();if (Process.supportsProcesses()) {throw new RuntimeException("Main thread loop unexpectedly exited");}thread.detach();String name = (thread.mInitialApplication != null)? thread.mInitialApplication.getPackageName(): "<unknown>";Slog.i(TAG, "Main thread of " + name + " is now exiting");}

         通过Looper.prepareMainLooper()/Looper.loop();建立消息循环,这就是主线程消息循环建立的地方。同时创建了ActivityThread对象,该对象代表了客户端MyService进程,然后调用thread.attach(false),注意参数为false。

        创建ActivityThread对象的时候其实还创建了IApplicationThread对象。

//frameworks\base\core\java\android\app\ActivityThread.java
final ApplicationThread mAppThread = new ApplicationThread();

         该对象代表了客户端,AMS就是通过这个对象与客户端通信的。attach的实现如下:

    private final void attach(boolean system) {sThreadLocal.set(this);mSystemThread = system;if (!system) {//1 ViewRoot.addFirstDrawHandler(new Runnable() {public void run() {ensureJitEnabled();}});android.ddm.DdmHandleAppName.setAppName("<pre-initialized>");RuntimeInit.setApplicationObject(mAppThread.asBinder());IActivityManager mgr = ActivityManagerNative.getDefault();try {mgr.attachApplication(mAppThread);} catch (RemoteException ex) {}} else {//2 // Don't set application object here -- if the system crashes,// we can't display an alert, we just want to die die die.android.ddm.DdmHandleAppName.setAppName("system_process");try {mInstrumentation = new Instrumentation();ContextImpl context = new ContextImpl();context.init(getSystemContext().mPackageInfo, null, this);Application app = Instrumentation.newApplication(Application.class, context);mAllApplications.add(app);mInitialApplication = app;app.onCreate();} catch (Exception e) {throw new RuntimeException("Unable to instantiate Application():" + e.toString(), e);}}ViewRoot.addConfigCallback(new ComponentCallbacks() {public void onConfigurationChanged(Configuration newConfig) {synchronized (mPackages) {// We need to apply this change to the resources// immediately, because upon returning the view// hierarchy will be informed about it.if (applyConfigurationToResourcesLocked(newConfig)) {// This actually changed the resources!  Tell// everyone about it.if (mPendingConfiguration == null ||mPendingConfiguration.isOtherSeqNewer(newConfig)) {mPendingConfiguration = newConfig;queueOrSendMessage(H.CONFIGURATION_CHANGED, newConfig);}}}}public void onLowMemory() {}});}

        这里说明以下两处:

  1. 参数system为false,所以会进入这个分支,然后调用attachApplication
  2. 这个分支表示system为true,代表了系统进程。

        由于system为false,所以流程走第一个分支,IActivityManager mgr = ActivityManagerNative.getDefault(),拿到AMS的代理,调用attachApplication;该函数调用会进入AMS侧,我们直接到AMS那边分析接下来的流程。

4、Service与AMS的关系再续

        进入AMS后,先调用attachApplication,代码如下:

    public final void attachApplication(IApplicationThread thread) {synchronized (this) {int callingPid = Binder.getCallingPid();final long origId = Binder.clearCallingIdentity();attachApplicationLocked(thread, callingPid);Binder.restoreCallingIdentity(origId);}}

        直接转发到函数 attachApplicationLocked,代码如下:

    private final boolean attachApplicationLocked(IApplicationThread thread,int pid) {//1 找到ProcessRecord对象,还记得该对象是啥时候加到集合mPidsSelfLocked的吗ProcessRecord app;if (pid != MY_PID && pid >= 0) {synchronized (mPidsSelfLocked) {app = mPidsSelfLocked.get(pid);}} else if (mStartingProcesses.size() > 0) {app = mStartingProcesses.remove(0);app.setPid(pid);} else {app = null;}if (app == null) {//...return false;}//...String processName = app.processName;//...//2 设置ProcessRecord对象的一些参数app.thread = thread;app.curAdj = app.setAdj = -100;app.curSchedGroup = Process.THREAD_GROUP_DEFAULT;app.setSchedGroup = Process.THREAD_GROUP_BG_NONINTERACTIVE;app.forcingToForeground = null;app.foregroundServices = false;app.debugging = false;//3 移除延时消息mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);//...try {//...//4 执行dex优化ensurePackageDexOpt(app.instrumentationInfo != null? app.instrumentationInfo.packageName: app.info.packageName);if (app.instrumentationClass != null) {ensurePackageDexOpt(app.instrumentationClass.getPackageName());}//5 回调应用程序thread.bindApplication(processName, app.instrumentationInfo != null? app.instrumentationInfo : app.info, providers,app.instrumentationClass, app.instrumentationProfileFile,app.instrumentationArguments, app.instrumentationWatcher, testMode, isRestrictedBackupMode || !normalMode,mConfiguration, getCommonServicesLocked());updateLruProcessLocked(app, false, true);app.lastRequestedGc = app.lastLowMemory = SystemClock.uptimeMillis();} catch (Exception e) {//...return false;}//....boolean badApp = false;boolean didSomething = false;// 6 See if the top visible activity is waiting to run in this process...ActivityRecord hr = mMainStack.topRunningActivityLocked(null);if (hr != null && normalMode) {if (hr.app == null && app.info.uid == hr.info.applicationInfo.uid&& processName.equals(hr.processName)) {try {if (mMainStack.realStartActivityLocked(hr, app, true, true)) {didSomething = true;}} catch (Exception e) {Slog.w(TAG, "Exception in new application when starting activity "+ hr.intent.getComponent().flattenToShortString(), e);badApp = true;}} else {mMainStack.ensureActivitiesVisibleLocked(hr, null, processName, 0);}}// 7 Find any services that should be running in this process...if (!badApp && mPendingServices.size() > 0) {ServiceRecord sr = null;try {for (int i=0; i<mPendingServices.size(); i++) {sr = mPendingServices.get(i);if (app.info.uid != sr.appInfo.uid|| !processName.equals(sr.processName)) {continue;}mPendingServices.remove(i);i--;realStartServiceLocked(sr, app);didSomething = true;}} catch (Exception e) {Slog.w(TAG, "Exception in new application when starting service "+ sr.shortName, e);badApp = true;}}// 8 Check if the next broadcast receiver is in this process...BroadcastRecord br = mPendingBroadcast;if (!badApp && br != null && br.curApp == app) {try {mPendingBroadcast = null;processCurBroadcastLocked(br, app);didSomething = true;} catch (Exception e) {Slog.w(TAG, "Exception in new application when starting receiver "+ br.curComponent.flattenToShortString(), e);badApp = true;logBroadcastReceiverDiscardLocked(br);finishReceiverLocked(br.receiver, br.resultCode, br.resultData,br.resultExtras, br.resultAbort, true);scheduleBroadcastsLocked();// We need to reset the state if we fails to start the receiver.br.state = BroadcastRecord.IDLE;}}//...return true;}

        代码中标记了一些关键的地方,下面一一说明:

  1. 找到ProcessRecord对象,主要是从mPidsSelfLocked集合中找到pid对应的ProcessRecord对象。
  2. 设置ProcessRecord对象的一些参数,其中最主要的是设置thread参数,该参数是IApplicationThread对象,代表了客户端;AMS就是通过这个对象与客户端通信的。
  3. 移除延时消息PROC_START_TIMEOUT_MSG,此时代表了新进程以及成功启动。
  4. 调用ensurePackageDexOpt执行dex优化,这里我们不关注。
  5. 调用bindApplication回调客户端,这里会调用到ActivityThread.handleBindApplication函数,然后执行Application的onCreate函数。从这里我们知道Application的onCreate运行于主线程中,所以我们在开发APP的时候最好只在里面做一些必要的初始化,否则会影响APP的启动速度。
  6. 此处是启动activity的,我们这里不关注。
  7. 启动service,这是我们需要关注的部分;还记得调用bringUpServiceLocked的时候mPendingServices集合加入了我们要启动的ServiceRecord对象吗?这里取出ServiceRecord对象,然后调用realStartServiceLocked继续启动服务。
  8. 广播处理,我们这里也不关注。

        realStartServiceLocked以ServiceRecord及ProcessRecord为参数,代码如下:

    private final void realStartServiceLocked(ServiceRecord r,ProcessRecord app) throws RemoteException {//...r.app = app;r.restartTime = r.lastActivity = SystemClock.uptimeMillis();app.services.add(r);bumpServiceExecutingLocked(r, "create");updateLruProcessLocked(app, true, true);boolean created = false;try {//...//1 回调客户端app.thread.scheduleCreateService(r, r.serviceInfo);r.postNotification();created = true;} finally {if (!created) {app.services.remove(r);scheduleServiceRestartLocked(r, false);}}requestServiceBindingsLocked(r);//2设置一些参数if (r.startRequested && r.callStart && r.pendingStarts.size() == 0) {r.lastStartId++;if (r.lastStartId < 1) {r.lastStartId = 1;}r.pendingStarts.add(new ServiceRecord.StartItem(r, r.lastStartId, null, -1));}//3 回调客户端onStartsendServiceArgsLocked(r, true);}

        scheduleCreateService将会回调到客户端,进入到ActivityThead.handleCreateService,而sendServiceArgsLocked将会回调客户端的onStartCommond方法。

        先来看看handleCreateService函数,代码如下:

    private final 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);Service service = null;try {//1java.lang.ClassLoader cl = packageInfo.getClassLoader();service = (Service) cl.loadClass(data.info.name).newInstance();} catch (Exception e) {//...}try {ContextImpl context = new ContextImpl();context.init(packageInfo, null, this);Application app = packageInfo.makeApplication(false, mInstrumentation);context.setOuterContext(service);service.attach(context, this, data.info.name, data.token, app,ActivityManagerNative.getDefault());//2service.onCreate();mServices.put(data.token, service);try {ActivityManagerNative.getDefault().serviceDoneExecuting(data.token, 0, 0, 0);} catch (RemoteException e) {// nothing to do.}} catch (Exception e) {//...}}

         这里比较简单,首先loadClass加载了我们实现的服务类,然后调用服务的onCreate方法,嗯,终于看到了熟悉的onCreate了。

        好啦,感觉这篇有点长了,先不写了。

        这里只是回调到了服务的onCreate,我们知道在服务的onCreate里面执行耗时任务会导致ANR,下一篇我们将继续分析服务的生命周期调用是如何在AMS侧实现的、同时分析为何onCreate、onStartCommond执行耗时任务会导致ANR。

相关文章:

Android中Service在新进程中的启动流程2

目录 1、Service在客户端的启动入口 2、Service启动在AMS的处理 3、Service在新进程中的启动 4、Service与AMS的关系再续 上一篇文章中我们了解了Service在新进程中启动的大致流程&#xff0c;同时认识了与客户端进程交互的接口IApplicationThread以及与AMS交互的接口IActi…...

C语言初阶力扣刷题——349. 两个数组的交集【难度:简单】

1. 题目描述 力扣在线OJ题目 给定两个数组&#xff0c;编写一个函数来计算它们的交集。 示例&#xff1a; 输入&#xff1a;nums1 [1,2,2,1], nums2 [2,2] 输出&#xff1a;[2] 输入&#xff1a;nums1 [4,9,5], nums2 [9,4,9,8,4] 输出&#xff1a;[9,4] 2. 思路 直接暴力…...

Java 大视界 -- Java 大数据在自动驾驶中的数据处理与决策支持(68)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…...

DeepSeek学术写作测评第二弹:数据分析、图表解读,效果怎么样?

我是娜姐 迪娜学姐 &#xff0c;一个SCI医学期刊编辑&#xff0c;探索用AI工具提效论文写作和发表。 针对最近全球热议的DeepSeek开源大模型&#xff0c;娜姐昨天分析了关于论文润色、中译英的详细效果测评&#xff1a; DeepSeek学术写作测评第一弹&#xff1a;论文润色&#…...

(2)SpringBoot自动装配原理简介

SpringBoot自动装配 这里写目录标题 SpringBoot自动装配启动器主程序自定义扫描包SpringBootApplicationSpringBootConfigurationEnableAutoConfigurationAutoConfigurationPackageImport({AutoConfigurationImportSelector.class})选择器AutoConfigurationEntrygetCandidateCo…...

Rust:Rhai脚本编程示例

当然&#xff0c;以下是一个简单的Rhai脚本编程示例&#xff0c;展示了如何在Rust中使用Rhai执行脚本。 首先&#xff0c;你需要确保你的Rust项目中包含了rhai库。你可以在你的Cargo.toml文件中添加以下依赖项&#xff1a; [dependencies] rhai "0.19" # 请检查最…...

深入理解文件描述符

问题 文件描述符只是一个整数值&#xff0c;那么系统是如何利用这个整数值来完成文件读写的呢&#xff1f; 什么是文件系统&#xff1f; 计算机中用于组织、存储和管理文件的数据结构集合 管理磁盘或其他存储介质上的空间 (将存储介质分块管理)保证文件数据不被破坏&#xf…...

使用CSS实现一个加载的进度条

文章目录 使用CSS实现一个加载的进度条一、引言二、步骤一&#xff1a;HTML结构与CSS基础样式1、HTML结构2、CSS基础样式 三、步骤二&#xff1a;添加动画效果1、使用CSS动画2、结合JavaScript控制动画 四、使用示例五、总结 使用CSS实现一个加载的进度条 一、引言 在现代网页…...

SQL 指南

SQL 指南 引言 SQL(Structured Query Language,结构化查询语言)是一种用于管理关系数据库系统的标准计算机语言。自1970年代问世以来,SQL已经成为了数据库管理和数据操作的事实标准。本文旨在为初学者和有经验的数据库用户提供一个全面的SQL指南,涵盖SQL的基础知识、高级…...

sqlzoo答案4:SELECT within SELECT Tutorial

sql练习&#xff1a;SELECT within SELECT Tutorial - SQLZoo world表&#xff1a; namecontinentareapopulationgdpAfghanistanAsia6522302550010020343000000AlbaniaEurope28748283174112960000000AlgeriaAfrica238174137100000188681000000AndorraEurope46878115371200000…...

斐波那契数(信息学奥赛一本通-1071)

【题目描述】 菲波那契数列是指这样的数列: 数列的第一个和第二个数都为1&#xff0c;接下来每个数都等于前面2个数之和。给出一个正整数k&#xff0c;要求菲波那契数列中第k个数是多少。 【输入】 输入一行&#xff0c;包含一个正整数k。&#xff08;1 ≤ k ≤ 46&#xff09;…...

数据结构与算法再探(六)动态规划

目录 动态规划 (Dynamic Programming, DP) 动态规划的基本思想 动态规划的核心概念 动态规划的实现步骤 动态规划实例 1、爬楼梯 c 递归&#xff08;超时&#xff09;需要使用记忆化递归 循环 2、打家劫舍 3、最小路径和 4、完全平方数 5、最长公共子序列 6、0-1背…...

ECMAScript--promise的使用

​ 一、Promise的简介 Promise是一个代理&#xff0c;它所代表的值在创建时并不一定是已知的。借助Promise&#xff0c;我们能够将处理程序与异步操作最终的成功值或者失败原因关联起来。这一特性使得异步方法可以像同步方法那样返回值&#xff0c;不同之处在于异步方法不会立…...

微服务入门(go)

微服务入门&#xff08;go&#xff09; 和单体服务对比&#xff1a;里面的服务仅仅用于某个特定的业务 一、领域驱动设计&#xff08;DDD&#xff09; 基本概念 领域和子域 领域&#xff1a;有范围的界限&#xff08;边界&#xff09; 子域&#xff1a;划分的小范围 核心域…...

【自学笔记】计算机网络的重点知识点-持续更新

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 计算机网络重点知识点一、计算机网络概述二、网络分类三、网络性能指标四、网络协议与体系结构五、数据交换方式六、物理层与数据链路层七、网络层与运输层八、应用…...

leetcode——二叉树的中序遍历(java)

给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,3,2] 示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[] 示例 3&#xff1a; 输入&#xff1a;root [1] 输出…...

neo4j-community-5.26.0 install in window10

在住处电脑重新配置一下neo4j, 1.先至官方下载 Neo4j Desktop Download | Free Graph Database Download Neo4j Deployment Center - Graph Database & Analytics 2.配置java jdk jdk 21 官网下载 Java Downloads | Oracle 中国 path: 4.查看java -version 版本 5.n…...

物联网智能项目之——智能家居项目的实现!

成长路上不孤单&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a; 【14后&#x1f60a;///计算机爱好者&#x1f60a;///持续分享所学&#x1f60a;///如有需要欢迎收藏转发///&#x1f60a;】 今日分享关于物联网智能项目之——智能家居项目…...

基于SpringBoot的假期周边游平台的设计与实现(源码+SQL脚本+LW+部署讲解等)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…...

JavaScript_03 超简计算器

版本一: <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>计算器</title><script type"text/javascript">function add(){let num1 document.getElementById("number1&qu…...

【重生之我在学习C语言指针详解】

目录 ​编辑 --------------------------------------begin---------------------------------------- 引言 一、指针基础 1.1 内存地址 1.2 指针变量 1.3 指针声明 1.4 取地址运算符 & 1.5 解引用运算符 *** 二、指针运算 2.1 指针加减运算 2.2 指针关系运算 三…...

深度学习每周学习总结R5(LSTM-实现糖尿病探索与预测-模型优化)

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客R7中的内容&#xff0c;为了便于自己整理总结起名为R5&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 目录 0. 总结优化细节&#xff08;目前只采用了1、2两种方式&#xff09;1. L2 正则…...

单元测试在复杂业务逻辑开发中的重要性与实践

背景 以前编写程序时&#xff0c;我并没有养成大量撰写单元测试的习惯&#xff0c;尤其是在写偏向业务代码的情况下&#xff0c;写的单元测试很少&#xff0c;只有在封装一些公共方法的时候才会写一些测试用例。 然而&#xff0c;最近我在开发的一个业务时&#xff0c;深刻地…...

Kubernetes 环境中的自动化运维实战指南

Kubernetes 作为容器编排领域的领导者,已经成为云原生应用的核心基础设施。然而,随着集群规模的扩大和应用的复杂化,手动运维 Kubernetes 集群变得愈发困难。自动化运维成为提升效率、保障系统稳定性的关键。本文将详细介绍如何在 Kubernetes 环境中实施自动化运维,涵盖工具…...

Linux 如何使用fdisk进行磁盘相关的操作

简介 fdisk 命令是 Linux 中用于管理磁盘分区的强大文本实用程序。它可以创建、删除、调整大小和修改硬盘上的分区。 基本语法 fdisk [options] <device> <device>&#xff1a;要管理的磁盘&#xff0c;例如 /dev/sda、/dev/nvme0n1 或 /dev/vda 示例用法 列…...

嵌入式Linux:如何监视子进程

目录 1、wait()函数 2、waitpid()函数 3、SIGCHLD信号 在嵌入式Linux系统中&#xff0c;父进程通常需要创建子进程来执行特定任务&#xff0c;例如处理网络请求、执行计算任务等。监视子进程的状态不仅可以确保资源的合理利用&#xff0c;还能防止僵尸进程的产生&#xff0c…...

【信息系统项目管理师-选择真题】2010上半年综合知识答案和详解

更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 【第1~2题】【第3题】【第4题】【第5题】【第6题】【第7题】【第8题】【第9题】【第10题】【第11题】【第12题】【第13题】【第14题】【第15题】【第16题】【第17题】【第18题】【第19题】【第20题】【第21题】…...

工作总结:压测篇

前言 压测是测试需要会的一项技能&#xff0c;作为开发&#xff0c;有点时候也要会一点压测。也是被逼着现学现卖的。 一、压测是什么&#xff0c;以及压测工具的选择 压测&#xff0c;即压力测试&#xff0c;是一种性能测试手段&#xff0c;通过模拟大量用户同时访问系统&am…...

doris:STRUCT

STRUCT<field_name:field_type [COMMENT comment_string], ... > 表示由多个 Field 组成的结构体&#xff0c;也可被理解为多个列的集合。 不能作为 Key 使用&#xff0c;目前 STRUCT 仅支持在 Duplicate 模型的表中使用。一个 Struct 中的 Field 的名字和数量固定&…...

二叉树介绍

一.树的概念 树的图&#xff1a; 1.结点的度&#xff1a;一个结点含有子树的个数称为该结点的度&#xff1b; 如上图&#xff1a;A的度为6 2.树的度&#xff1a;一棵树中&#xff0c;所有结点度的最大值称为树的度&#xff1b; 如上图&#xff1a;树的度为6 3.叶子结点或终…...

通过Ngrok实现内网穿透助力远程开发

在现代软件开发和网络应用的环境下&#xff0c;开发人员常常需要在本地搭建服务器进行调试、测试或演示。然而&#xff0c;传统的端口映射&#xff08;如使用 NAT 或 SSH 隧道&#xff09;配置繁琐&#xff0c;且并非所有环境都允许直接暴露本地服务。ngrok 作为一款强大的隧道…...

DeepSeek-R1:通过强化学习激励大型语言模型(LLMs)的推理能力

摘要 我们推出了第一代推理模型&#xff1a;DeepSeek-R1-Zero和DeepSeek-R1。DeepSeek-R1-Zero是一个未经监督微调&#xff08;SFT&#xff09;作为初步步骤&#xff0c;而是通过大规模强化学习&#xff08;RL&#xff09;训练的模型&#xff0c;展现出卓越的推理能力。通过强…...

Node.js基础

浏览器知识 浏览器 个浏览器都内置了DOM、BOM等API函数&#xff0c;供浏览器中的Javascript调用。 每个浏览器都有对应的JavaScript解析引擎。 浏览器中的JavaScript环境 V8引擎负责解析和执行JavaScript代码 内置API是由运行环境提供的特殊接口&#xff0c;只能在所属的运…...

DeepSeek R1:中国AI黑马的崛起与挑战

在人工智能&#xff08;AI&#xff09;领域&#xff0c;大型语言模型&#xff08;LLMs&#xff09;正以迅猛之势重塑世界&#xff0c;其发展速度和影响力令人瞩目。近期&#xff0c;中国DeepSeek公司发布的DeepSeek R1模型&#xff0c;宛如一颗璀璨新星&#xff0c;凭借卓越的推…...

【JavaEE】_MVC架构与三层架构

目录 1. MVC架构 2. 三层架构 3. MVC架构与三层架构的对比 3.1 MVC与三层架构的对比 3.2 MVC与三层架构的共性 1. MVC架构 在前文已介绍关于SpringMAC的设计模式&#xff0c;详见下文&#xff1a; 【JavaEE】_Spring Web MVC简介-CSDN博客文章浏览阅读967次&#xff0c;点…...

对比DeepSeek、ChatGPT和Kimi的学术写作摘要能力

摘要 摘要是文章的精华&#xff0c;通常在200-250词左右。要包括研究的目的、方法、结果和结论。让AI工具作为某领域内资深的研究专家&#xff0c;编写摘要需要言简意赅&#xff0c;直接概括论文的核心&#xff0c;为读者提供快速了解的窗口。 下面我们使用DeepSeek、ChatGPT…...

ts 进阶

吴悠讲编程 : 20分钟TypeScript进阶&#xff01;无废话快速提升水平 前端速看 https://www.bilibili.com/video/BV1q64y1j7aH...

Kubernetes(一)

Kubernetes&#xff08;简称K8s&#xff09;是一个开源的容器编排平台&#xff0c;已经成为现代云原生应用的核心技术&#xff0c;主要应用于对容器化应用程序的自动化部署、扩展以及管理。k8s配备了一组核心组件以及一系列功能&#xff0c;这些组件能够实现容器的调度、负载均…...

Python里的小整数问题挺有意思的

简单来说&#xff0c;Python为了优化性能&#xff0c;会把一些常用的整数&#xff08;通常是-5到256&#xff09;提前创建好&#xff0c;放到一个“缓存池”里。这样&#xff0c;当你用到这些小整数时&#xff0c;Python就不用每次都重新创建对象了&#xff0c;直接从缓存池里拿…...

基于 Jenkins 的测试报告获取与处理并写入 Jira Wiki 的技术总结

title: 基于 Jenkins 的测试报告获取与处理并写入 Jira Wiki 的技术总结 tags: - jenkins - python categories: - jenkins在软件开发的持续集成与持续交付&#xff08;CI/CD&#xff09;流程里&#xff0c;及时、准确地获取并分析测试报告对保障软件质量至关重要。本文将详细…...

java.util.Random类(详细案例拆解)(已完结)

前言&#xff1a; 小编打算近期更俩三期类的专栏&#xff0c;一些常用的专集类&#xff0c;给大家分好类别总结和详细的代码举例解释。 今天是除夕&#xff0c;小编先祝贺大家除夕快乐啦&#xff01;&#xff01; 今天是第六个 java.lang.Math 包中的 java.util.Random类 我…...

CMake常用命令指南(CMakeList.txt)

CMakeList从入门到精通的文章有很多不再赘述&#xff08; 此处附带一篇优秀的博文链接&#xff1a;一个简单例子&#xff0c;完全入门CMake语法与CMakeList编写 &#xff09;。 本文主要列举 CMake 中常用命令的详细说明、优缺点分析以及推荐做法&#xff0c;以更好地理解和灵…...

Mybatis是如何进行分页的?

大家好&#xff0c;我是锋哥。今天分享关于【Mybatis是如何进行分页的&#xff1f;】面试题。希望对大家有帮助&#xff1b; Mybatis是如何进行分页的&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 MyBatis 实现分页的方式有很多种&#xff0c;最常见…...

推动知识共享的在线知识库实施与优化指南

内容概要 在当今迅速发展的数字化时代&#xff0c;在线知识库的实施显得尤为重要。它不仅为企业提供了高效的信息存储与共享平台&#xff0c;还能够有效促进团队成员之间的协作与知识传递。通过集中管理企业内的各类知识资源&#xff0c;在线知识库帮助员工快速查找所需信息&a…...

【最后203篇系列】007 使用APS搭建本地定时任务

说明 最大的好处是方便。 其实所有任务的源头&#xff0c;应该都是通过定时的方式&#xff0c;在每个时隙发起轮询。当然在任务的后续传递中&#xff0c;可以通过CallBack或者WebHook的方式&#xff0c;以事件的形态进行。这样可以避免长任务执行的过程中进行等待和轮询。 总结…...

为AI聊天工具添加一个知识系统 之78 详细设计之19 正则表达式 之6

本文要点 要点 本项目设计的正则表达式 是一个 动态正则匹配框架。它是一个谓词系统&#xff1a;谓词 是运动&#xff0c;主语是“维度”&#xff0c;表语是 语言处理。主语的一个 双动结构。 Reg三大功能 语法验证、语义检查和 语用检验&#xff0c;三者 &#xff1a;语义约…...

三天急速通关JavaWeb基础知识:Day 1 后端基础知识

三天急速通关JavaWeb基础知识&#xff1a;Day 1 后端基础知识 0 文章说明1 Http1.1 介绍1.2 通信过程1.3 报文 Message1.3.1 请求报文 Request Message1.3.2 响应报文 Response Message 2 XML2.1 介绍2.2 利用Java解析XML 3 Tomcat3.1 介绍3.2 Tomcat的安装与配置3.3 Tomcat的项…...

代理模式 -- 学习笔记

代理模式学习笔记 什么是代理&#xff1f; 代理是一种设计模式&#xff0c;用户可以通过代理操作&#xff0c;而真正去进行处理的是我们的目标对象&#xff0c;代理可以在方法增强&#xff08;如&#xff1a;记录日志&#xff0c;添加事务&#xff0c;监控等&#xff09; 拿一…...

前端-Rollup

Rollup 是一个用于 JavaScript 的模块打包工具&#xff0c;它将小的代码片段编译成更大、更复杂的代码&#xff0c;例如库或应用程序。它使用 JavaScript 的 ES6 版本中包含的新标准化代码模块格式&#xff0c;而不是以前的 CommonJS 和 AMD 等特殊解决方案。ES 模块允许你自由…...

EtherCAT主站IGH-- 21 -- IGH之fsm_reboot.h/c文件解析

EtherCAT主站IGH-- 21 -- IGH之fsm_reboot.h/c文件解析 0 预览一 该文件功能`fsm_reboot.c` 文件功能函数预览二 函数功能介绍`fsm_reboot.c` 中主要函数的作用1. `ec_fsm_reboot_init`2. `ec_fsm_reboot_clear`3. `ec_fsm_reboot_single`4. `ec_fsm_reboot_all`5. `ec_fsm_reb…...