《ANR系列》--- 触发机制
Broadcast的ANR触发机制
Service的ANR触发机制
ContentProvider的ANR触发机制
当系统发生ANR时,通常它会蹦出提示弹框 ---> 应用程序无响应。此时你有两种选择:
1、关闭应用;
2、继续等待。
这其实是系统在告诉我们:啊,当前APP出问题了,咱暂时没法响应后续操作了哦!
那么这背后,系统是怎么知道后续操作没法执行了呢?
系统咋知道ANR场景?
其实,我们在操作APP,主要都是在四大组件上进行各种操作。所以系统也主要监听四大组件触发ANR的情况。
作为一套系统机制,最好不要对单一成员有特殊对待,而采用不一样的监听机制。
所以,四大组件在触发ANR时,都坚持“**埋雷 -> 拆雷 -> 暴雷”的原则。**
那么我们分别来看看四大组件是怎么坚持这个统一原则的。
以下分析代码从官网获取:
Broadcast
启动流程
广播分为:有序广播、无序广播、本地广播。其中,只有有序广播坚持了统一原则,所以我们的重点在有序广播。
所以我们从sendOrderedBroadcast() 入口开始,梳理整个广播执行的生命周期流程:

埋雷、拆雷、爆雷
首先,调用方经ContextImpl中转到系统进程的ActivityManagerService(AMS) 服务;
@Override public void sendBroadcast(Intent intent) { ...... try { intent.prepareToLeaveProcess(this); ActivityManager.getService().broadcastIntentWithFeature( mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType, null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, false, false, getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }
经过一轮各种权限校验(如:userId、action等)后,交给广播队列BroadcastQueue(这里会根据不同的flag,选择是前台广播队列,还是后台广播队列。flag是根据调用方的发送方式加入到Intent中的)
可以看到,前台广播队列和后台广播队列其实都是BroadcastQueue,但构造参数实参略有不同
-
name:广播队列名
-
constants:对应了超时时长:


因此才有了前台广播10s超时,后台广播60s超时。但这是单个广播的超时时长,如果是整个BroadcastQueue的话,则是2*receiver个数*timeout时长为超时时长。
-
allowDelayBehindServices:前台广播是false,后台广播是true:
/** * If true, we can delay broadcasts while waiting services to finish in the previous * receiver's process. */ final boolean mDelayBehindServices;
根据代码注释猜测:后台广播的话,若在前一个广播接收器进程中,有服务service需要停止的话,当前广播可以延迟执行。(一般咱好像也用不到,先忽略吧😛)
然后,BroadcastQueue收到处理广播消息后,通过Handler发送出去,进行排队等待处理(此处的Handler所在线程是AMS);
由processNextBroadcastLocked真正处理广播。首先处理无序广播,循环发送给广播接收者;
while (mParallelBroadcasts.size() > 0) { r = mParallelBroadcasts.remove(0); r.dispatchTime = SystemClock.uptimeMillis(); r.dispatchClockTime = System.currentTimeMillis(); //性能监控 if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { Trace.asyncTraceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, createBroadcastTraceTitle(r, BroadcastRecord.DELIVERY_PENDING), System.identityHashCode(r)); Trace.asyncTraceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, createBroadcastTraceTitle(r, BroadcastRecord.DELIVERY_DELIVERED), System.identityHashCode(r)); } final int N = r.receivers.size(); if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST, "Processing parallel broadcast [" + mQueueName + "] " + r); for (int i=0; i<N; i++) { Object target = r.receivers.get(i); if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Delivering non-ordered on [" + mQueueName + "] to registered " + target + ": " + r); //循环遍历发送给广播接收者 deliverToRegisteredReceiverLocked(r, (BroadcastFilter)target, false, i); } addBroadcastToHistoryLocked(r); if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST, "Done with parallel broadcast [" + mQueueName + "] " + r); }
接着就开始处理有序广播。关键的来了!首先若当前还有广播接收者,且当前时间大于整个广播队列的超时时长了的话,就会发送超时消息,强制停止该广播(根据代码注释了解到,这类广播是由于系统超时为及时响应所以还存在,这部分被挂起的广播此时就需要立刻停止)。
int numReceivers = (r.receivers != null) ? r.receivers.size() : 0; if (mService.mProcessesReady && r.dispatchTime > 0) { long now = SystemClock.uptimeMillis(); //当前广播还有接收者 if ((numReceivers > 0) && //当前时间大于整个BroadcastQueue的超时时长 (now > r.dispatchTime + (2*mTimeoutPeriod*numReceivers))) { Slog.w(TAG, "Hung broadcast [" + mQueueName + "] discarded after timeout failure:" + " now=" + now + " dispatchTime=" + r.dispatchTime + " startTime=" + r.receiverTime + " intent=" + r.intent + " numReceivers=" + numReceivers + " nextReceiver=" + r.nextReceiver + " state=" + r.state); //立刻发送超时消息,强制停止当前广播 broadcastTimeoutLocked(false); // forcibly finish this broadcast forceReceive = true; r.state = BroadcastRecord.IDLE; } }
咱此时的广播不属于这类,所以接着往下走:
if (r.receivers == null || r.nextReceiver >= numReceivers || r.resultAbort || forceReceive) { if (r.resultTo != null) { ...... if (sendResult) { ....... try { if (DEBUG_BROADCAST) { Slog.i(TAG_BROADCAST, "Finishing broadcast [" + mQueueName + "] " + r.intent.getAction() + " app=" + r.callerApp); } if (r.dispatchTime == 0) { // The dispatch time here could be 0, in case it's a parallel // broadcast but it has a result receiver. Set it to now. r.dispatchTime = now; } r.mIsReceiverAppRunning = true; //1、发送消息给广播接收者 performReceiveLocked(r, r.resultToApp, r.resultTo, new Intent(r.intent), r.resultCode, r.resultData, r.resultExtras, false, false, r.shareIdentity, r.userId, r.callingUid, r.callingUid, r.callerPackage, r.dispatchTime - r.enqueueTime, now - r.dispatchTime, 0, r.resultToApp != null ? r.resultToApp.mState.getCurProcState() : ActivityManager.PROCESS_STATE_UNKNOWN); ...... r.resultTo = null; } catch (RemoteException e) { r.resultTo = null; Slog.w(TAG, "Failure [" + mQueueName + "] sending broadcast result of " + r.intent, e); } } } if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Cancelling BROADCAST_TIMEOUT_MSG"); //2、拆雷 cancelBroadcastTimeoutLocked(); if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST, "Finished with ordered broadcast " + r); ...... }
然后通过performReceiveLocked发送消息给广播接收者,去执行操作。执行完后再调用cancelBroadcastTimeoutLocked操作,该操作就是“拆雷”。这怎么就拆雷了?还没埋呢。。。
这其实是因为每次广播接收者处理完广播后,会通知AMS处理完毕,AMS再告诉BroadcastQueue可以处理下一个广播了,所以此时拆的雷是上一个广播埋下的雷。(有的地方会说埋雷是在第5步的位置,但其实条件并不满足)。具体埋雷还在下面。
// Get the next receiver... int recIdx = r.nextReceiver++; // Keep track of when this receiver started, and make sure there // is a timeout message pending to kill it if need be. r.receiverTime = SystemClock.uptimeMillis(); r.scheduledTime[recIdx] = r.receiverTime; if (recIdx == 0) { r.dispatchTime = r.receiverTime; r.dispatchRealTime = SystemClock.elapsedRealtime(); r.dispatchClockTime = System.currentTimeMillis(); ...... //性能监控 if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { Trace.asyncTraceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, createBroadcastTraceTitle(r, BroadcastRecord.DELIVERY_PENDING), System.identityHashCode(r)); Trace.asyncTraceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, createBroadcastTraceTitle(r, BroadcastRecord.DELIVERY_DELIVERED), System.identityHashCode(r)); } if (DEBUG_BROADCAST_LIGHT) Slog.v(TAG_BROADCAST, "Processing ordered broadcast [" + mQueueName + "] " + r); } if (! mPendingBroadcastTimeoutMessage) { long timeoutTime = r.receiverTime + mConstants.TIMEOUT; if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Submitting BROADCAST_TIMEOUT_MSG [" + mQueueName + "] for " + r + " at " + timeoutTime); //埋雷! setBroadcastTimeoutLocked(timeoutTime); }
当我们获取下一个广播接收器后,并且没有发送过超时消息,则通过setBroadcastTimeoutLocked设置超时消息,也即埋雷!
好,到这埋雷,拆雷都完成了。那么若没及时拆雷就会爆了,爆雷又是如何的呢?
爆雷
超时过后,最终会走到:
final void broadcastTimeoutLocked(boolean fromMsg) { ...... Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "broadcastTimeoutLocked()"); try { long now = SystemClock.uptimeMillis(); BroadcastRecord r = mDispatcher.getActiveBroadcastLocked(); ...... // The ANR should only be triggered if we have a process record (app is non-null) if (!debugging && app != null) { //交由AMS处理ANR事件 mService.appNotResponding(app, timeoutRecord); } } finally { Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } }
如上,爆雷后通知AMS处理ANR事件
☀️由此,有序广播完成了:埋雷 -> 拆雷 -> 暴雷的过程。
Service
启动流程
启动服务分为startService和bindService。最终其实都殊途同归,所以我们以startService为例:

埋雷、拆雷、爆雷
首先,同样是经过ContextImpl代理给系统进程的ActivityManagerService(AMS)处理。AMS也不会自己处理(毕竟作为大管家要管的事太多了),而是将这部分的职责交给ActiveServices处理。
由ActiveServices.startServiceLocked() 真正开始处理服务启动。
第一步国际惯例,确认调用方是否OK,将调用方封装成ServiceLookupResult实体,该实体内包含了对应的ServiceRecord记录,记载了Service调用方的元信息。
然后也是国际惯例,各种权限校验,检查,通知BatteryStatsService服务启动了(或许是功耗监控?先不管)。经bringUpServiceLocked执行拉起服务操作(这里会加上Trace监控)。
这里会有区分了,若Service所在进程已经存在了,则直接调用realStartServiceLocked()执行真正拉起服务操作;若Service所在进程还没启动,则通过AMS通知Zygote启动目标进程,目标进程启动后与AMS绑定再调用realStartServiceLocked() 往下走。
所以,无论如何接着realStartServiceLocked()继续。(该操作也是拉起服务的核心操作)
private void realStartServiceLocked(ServiceRecord r, ProcessRecord app, IApplicationThread thread, int pid, UidRecord uidRecord, boolean execInFg, boolean enqueueOomAdj, @ServiceBindingOomAdjPolicy int serviceBindingOomAdjPolicy) throws RemoteException { //客户端不在了,则抛异常退出 if (thread == null) { throw new RemoteException(); } if (DEBUG_MU) Slog.v(TAG_MU, "realStartServiceLocked, ServiceRecord.uid = " + r.appInfo.uid + ", ProcessRecord.uid = " + app.uid); r.setProcess(app, thread, pid, uidRecord); r.restartTime = r.lastActivity = SystemClock.uptimeMillis(); final boolean skipOomAdj = (serviceBindingOomAdjPolicy & SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CREATE) != 0; final ProcessServiceRecord psr = app.mServices; final boolean newService = psr.startService(r); //埋雷!! bumpServiceExecutingLocked(r, execInFg, "create", OOM_ADJ_REASON_NONE /* use "none" to avoid extra oom adj */, skipOomAdj /* skipTimeoutIfPossible */); mAm.updateLruProcessLocked(app, false, null); updateServiceForegroundLocked(psr, /* oomAdj= */ false); ...... boolean created = false; try { if (LOG_SERVICE_START_STOP) { ...... //日志记录Service启动 EventLogTags.writeAmCreateService( r.userId, System.identityHashCode(r), nameTerm, r.app.uid, pid); } final int uid = r.appInfo.uid; final String packageName = r.name.getPackageName(); final String serviceName = r.name.getClassName(); ...... //通知APP进程,执行创建Service操作。thread就是客户端的ApplicationThread thread.scheduleCreateService(r, r.serviceInfo, null /* compatInfo (unused but need to keep method signature) */, app.mState.getReportedProcState()); r.postNotification(false); created = true; } catch (DeadObjectException e) { ...... } finally { ...... } ...... requestServiceBindingsLocked(r, execInFg, serviceBindingOomAdjPolicy); ...... } //bumpServiceExecutingLocked -> scheduleServiceTimeoutLocked执行埋雷 void scheduleServiceTimeoutLocked(ProcessRecord proc) { if (proc.mServices.numberOfExecutingServices() == 0 || proc.getThread() == null) { return; } //根据前台服务还是后台服务设置不同的delay时间 final long delay = proc.mServices.shouldExecServicesFg() ? mAm.mConstants.SERVICE_TIMEOUT : mAm.mConstants.SERVICE_BACKGROUND_TIMEOUT; mActiveServiceAnrTimer.start(proc, delay); proc.mServices.noteScheduleServiceTimeoutPending(false); }
由此,可见在通知客户端进程创建Service之前,已经准备好了埋雷操作,并根据前台服务20s,后台服务200s 的要求设置爆雷延迟时间。
然后,进入到客户端进程。客户端进程的Binder收到接口调用:
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; //通过Handler将创建Service的任务加入到消息队列中 sendMessage(H.CREATE_SERVICE, s); }
轮到创建Service执行时:
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(); final LoadedApk packageInfo = getPackageInfoNoCheck(data.info.applicationInfo); Service service = null; try { if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name); Application app = packageInfo.makeApplicationInner(false, mInstrumentation); final java.lang.ClassLoader cl; //1、获取加载Service的类加载器 if (data.info.splitName != null) { cl = packageInfo.getSplitClassLoader(data.info.splitName); } else { cl = packageInfo.getClassLoader(); } //2、反射获取Service实例 service = packageInfo.getAppFactory() .instantiateService(cl, data.info.name, data.intent); //3、创建Service的上下文context ContextImpl context = ContextImpl.getImpl(service .createServiceBaseContext(this, packageInfo)); if (data.info.splitName != null) { context = (ContextImpl) context.createContextForSplit(data.info.splitName); } if (data.info.attributionTags != null && data.info.attributionTags.length > 0) { final String attributionTag = data.info.attributionTags[0]; context = (ContextImpl) context.createAttributionContext(attributionTag); } // Service resources must be initialized with the same loaders as the application // context. //4、Service的context的资源需要加载所在进程的resources context.getResources().addLoaders( app.getResources().getLoaders().toArray(new ResourcesLoader[0])); context.setOuterContext(service); //5、Service对象绑定Context service.attach(context, this, data.info.name, data.token, app, ActivityManager.getService()); ...... //6、回调Servie.onCreate() service.onCreate(); mServicesData.put(data.token, data); mServices.put(data.token, service); try { //7、通知AMS,Service已经创建完毕 ActivityManager.getService().serviceDoneExecuting( data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } catch (Exception e) { ...... } }
整个过程可概括为:
1、获取当前应用的类加载器;
2、通过类加载器反射获取Service对象;
3、创建Service的上下文context,其context添加App的resources;
4、Service对象和其context绑定;
5、回调Service.onCreate();
6、通知AMS,客户端的Service已经创建完毕。
此时,又回调system_server进程,AMS收到通知后转而叫告诉ActiveServices了(不是我的活,坚决不接😛)。
ActiveServices接过接力棒之后,最终在serviceDoneExecutingLocked进行拆雷(取消ANR定时器):
private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying, boolean finishing, boolean enqueueOomAdj, @OomAdjReason int oomAdjReason) { if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "<<< DONE EXECUTING " + r + ": nesting=" + r.executeNesting + ", inDestroying=" + inDestroying + ", app=" + r.app); else if (DEBUG_SERVICE_EXECUTING) Slog.v(TAG_SERVICE_EXECUTING, "<<< DONE EXECUTING " + r.shortInstanceName); r.executeNesting--; if (r.executeNesting <= 0) { if (r.app != null) { final ProcessServiceRecord psr = r.app.mServices; if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Nesting at 0 of " + r.shortInstanceName); psr.setExecServicesFg(false); psr.stopExecutingService(r); if (psr.numberOfExecutingServices() == 0) { if (DEBUG_SERVICE || DEBUG_SERVICE_EXECUTING) Slog.v(TAG_SERVICE_EXECUTING, "No more executingServices of " + r.shortInstanceName); //目标进程还在,则取消对应的ANR定时器 if (r.app.mPid != 0) mActiveServiceAnrTimer.cancel(r.app); } else if (r.executeFg) { // Need to re-evaluate whether the app still needs to be in the foreground. for (int i = psr.numberOfExecutingServices() - 1; i >= 0; i--) { if (psr.getExecutingServiceAt(i).executeFg) { psr.setExecServicesFg(true); break; } } } ...... } ...... } }
有了,埋雷、拆雷,那爆雷呢?
当AnrTimer时间到了后,就会通过AMS的handler发送超时消息,通知AMS处理ANR操作。当然,AMS又丢给ActiveServies了。。。
void serviceTimeout(ProcessRecord proc) { try { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceTimeout()"); TimeoutRecord timeoutRecord = null; ...... if (timeoutRecord != null) { //交由AnrHelper处理ANR超时消息 mAm.mAnrHelper.appNotResponding(proc, timeoutRecord); } } finally { Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } }
☀️由此,Service完成了埋雷、拆雷、爆雷的操作
ContentProvider
ContentProvider的ANR,是不是感觉没咋遇到过?没错,我们一般使用的ContentProvider发生ANR时是不会弹ANR弹框,也不会dump相应的ANR日志的,它只会把发生ANR的进程给。。。干掉!
在我们的APP启动时,ContentProvider就会启动了,其埋雷也正在此时进行:
//ContentProvider和App绑定 private final boolean attachApplicationLocked(IApplicationThread thread, int pid) { ProcessRecord app; if (pid != MY_PID && pid >= 0) { synchronized (mPidsSelfLocked) { app = mPidsSelfLocked.get(pid); // 根据pid获取ProcessRecord } } ... //系统处于ready状态或者该app为FLAG_PERSISTENT进程则为true boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info); List<ProviderInfo> providers = normalMode ? generateApplicationProvidersLocked(app) : null; //app进程存在正在启动中的provider,则超时10s后发送CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG消息 if (providers != null && checkAppInLaunchingProvidersLocked(app)) { Message msg = mHandler.obtainMessage(CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG); msg.obj = app; mHandler.sendMessageDelayed(msg, CONTENT_PROVIDER_PUBLISH_TIMEOUT); } thread.bindApplication(...); ... }
没错,在ContentProvider启动时和App绑定过程中就通过Handler发送了超时消息了,也就是在这里埋了雷。
然后通过publish的方式告诉AMS处理事件,当然AMS又当了甩手掌柜,交给ContentProviderHelper处理。拆雷也正在此处进行:
/** * List of content providers who have clients waiting for them. The * application is currently being launched and the provider will be * removed from this list once it is published. */ //粗略理解:mLaunchingProviders存储的是启动的ContentProvider。但当CP被发布(启动)后,就会被移除 private final ArrayList<ContentProviderRecord> mLaunchingProviders = new ArrayList<>(); void publishContentProviders(IApplicationThread caller , List<ContentProviderHolder> providers) { if (providers == null) { return; } mService.enforceNotIsolatedOrSdkSandboxCaller("publishContentProviders"); synchronized (mService) { final ProcessRecord r = mService.getRecordForAppLOSP(caller); if (DEBUG_MU) { Slog.v(TAG_MU, "ProcessRecord uid = " + r.uid); } if (r == null) { throw new SecurityException("Unable to find app for caller " + caller + " (pid=" + Binder.getCallingPid() + ") when publishing content providers"); } final long origId = Binder.clearCallingIdentity(); boolean providersPublished = false; for (int i = 0, size = providers.size(); i < size; i++) { ContentProviderHolder src = providers.get(i); if (src == null || src.info == null || src.provider == null) { continue; } ContentProviderRecord dst = r.mProviders.getProvider(src.info.name); if (dst == null) { continue; } if (DEBUG_MU) { Slog.v(TAG_MU, "ContentProviderRecord uid = " + dst.uid); } providersPublished = true; ComponentName comp = new ComponentName(dst.info.packageName, dst.info.name); mProviderMap.putProviderByClass(comp, dst); String[] names = dst.info.authority.split(";"); for (int j = 0; j < names.length; j++) { mProviderMap.putProviderByName(names[j], dst); } boolean wasInLaunchingProviders = false; for (int j = 0, numLaunching = mLaunchingProviders.size(); j < numLaunching; j++) { if (mLaunchingProviders.get(j) == dst) { mLaunchingProviders.remove(j); wasInLaunchingProviders = true; j--; numLaunching--; } } //成功publish,则移除超时消息 if (wasInLaunchingProviders) { mService.mHandler.removeMessages( ActivityManagerService.WAIT_FOR_CONTENT_PROVIDER_TIMEOUT_MSG , dst); mService.mHandler.removeMessages( ActivityManagerService.CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG , r); } ...... } ...... Binder.restoreCallingIdentity(origId); } }
publishContentProviders() 是在app启动后,发布ContentProvider时调用的方法,只有发布成功了,系统才能识别到,APP才能使用到。所以这整个过程是:在ContentProvider启动时埋雷,到发布成功后再拆雷。后续在CRUD或call就不涉及了。
那埋了雷,拆了雷。爆雷干了啥?
//AMS收到超时消息后,转交给ContentProviderHelper处理 void processContentProviderPublishTimedOutLocked(ProcessRecord app) { cleanupAppInLaunchingProvidersLocked(app, true); mService.mProcessList.removeProcessLocked(app, false, true, ApplicationExitInfo.REASON_INITIALIZATION_FAILURE, ApplicationExitInfo.SUBREASON_UNKNOWN, "timeout publishing content providers"); }
可以看到,爆雷后就是清除启动的进程,并从对应的缓存中移除。
以上,就是常见的3大组件的ANR触发机制原理了。
等下,说好的四大组件呢?怎么就只有3个?Are you kidding me???
其实我们在Activity主线程中遇到的ANR,通常会结合Input系统分析,通常是主线程阻塞然后喂了一个input事件后,就爆出了经典的Input Timeout了。(顺带一提,Activity的ANR埋雷在Activity切换流程中。只是我目前还没遇到过Activity本身暴雷的🤣,也未曾见过各路豪杰有相关的分享,或许确实极少遇到。若有大佬有相关文章,欢迎推荐~)
浙公网安备 33010602011771号