Android BroadcastReceiver 相关笔记整理

涉及源码版本为 Android API 25

参照文章:
1、《Android 开发艺术探索》
2、BroadcastReceiver 全方位解析


1、广播接收器的注册
1.1 静态注册

在 AndroidManifest.xml 里通过标签声明:

<receiver
  // 定义系统是否能够实例化这个广播接收器,如果为 false 的话,则系统无法实例化该接收器。
  // 此时,就只能在代码中主动实例化该对象并动态注册
  android:enabled=["true" | "false"]
  android:exported=["true" | "false"]
  android:icon="drawable resource"
  android:label="string resource"
  // 继承 BroadcastReceiver 子类的类名(包含路径名)
  android:name=".XXXBroadcastReceiver"
  // 具有相应权限的广播发送者发送的广播才能被此 BroadcastReceiver 所接收;
  android:permission="string"
  // BroadcastReceiver 运行所处的进程,默认为 app 的进程,可以指定独立的进程
  android:process="string" >

  // 用于指定此广播接收器将接收的广播类型
  // 本示例中给出的是用于接收网络状态改变时发出的广播
  <intent-filter>
    <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
  </intent-filter>
</receiver>

其中对于 android:exported 属性,有:

这个属性用于指示该广播接收器是否能够接收来自应用程序外部的消息,如果设置 true,则能够接收,如果设置为 false,则不能够接收。如果设置为 false,则该接收只能接收那些由相同应用程序组件或带有相同用户 ID 的应用程序所发出的消息。

它的默认值依赖它所包含的 intent-filter。如果不包含过滤器,则接收器只能由指定了明确类名的 Intent 对象来调用,这就意味着该接收器只能在应用程序内部使用(因为通常在应用程序外部是不会知道这个类名的)。这种情况下默认值就是 false。另一方面,如果接受器至少包含了一个过滤器,那么就意味着这个接收器能够接收来自系统或其他应用程序的 Intent 对象,因此默认值是true。

摘抄自:AndroidManifest.xml文件详解(receiver)

1.2 动态注册
// 动态注册
val receiver = MyReceiver()
val intentFilter = IntentFilter()
intentFilter.addAction(...)

// 动态注销
unregisterReceiver(receiver)

其中:

  1. 对于同一个广播接收器对象,可以重复注册,重复注册时 IntentFilter 可以是同一对象,也可以是不同对象。

    针对同一个接收器对象,注册不同的 IntentFilter 对象,但是如果不同的 IntentFilter 对象中存在同样的 action 值,此时发送该 action 值的广播一次,同一广播接收器对象的 onReceive() 方法也只会响应一次。

    推测上述情况,当发送一次广播的时候,系统应该对于同样的 action 值只触发一次同一广播接收器对象的 onReceive() 方法。

  2. 但是不能重复解除注册,应该第一次解除之后,对应的对象就被清除了,再次解除时已经不存在了。此时会产生异常 java.lang.IllegalArgumentException: Receiver not registered: XXXReceiver

  3. 广播可以设置监听多个 action,因为 IntentFilter 可以添加多个 action。

1.3 补充

在 Android 5.0 中,默认情况下广播不会发送给已经停止的应用,其实在 3.1 开始就已经具备这种特性。

因为在 3.1 中就为 Intent 添加了两个标记位,分别是 FLAG_INCLUDE_STOPPED_PACKAGESFLAG_EXCLUDE_STOPPED_PACKAGES,用来控制广播是否要对处于停止状态的应用其作用。

从 3.1 起,系统为所有广播默认添加了 FLAG_EXCLUDE_STOPPED_PACKAGES,以防止广播无意间或者在不必要的时候调起已经运行的应用。两种标记共存时,以 FLAG_INCLUDE_STOPPED_PACKAGES 为准。

一个应用处于停止状态分为两种情况:
(1)安装后未运行;
(2)应用被手动或者其他应用强停了。

Android 3.1 中广播的这个特性同样会影响开机广播,3.1 之前,处于停止状态的应用是可以收到开机广播的,但是 3.1 开始,处于停止状态的应用无法接收开机广播。

2、广播的类型与发送

具体参阅:
https://lrh1993.gitbooks.io/android_interview_guide/content/android/basis/broadcastreceiver.html#432-%E5%B9%BF%E6%92%AD%E7%9A%84%E7%B1%BB%E5%9E%8B

其中,黏性广播在 Android 5.0 被废弃。而黏性广播是指广播在发出后,还会保存在 AMS 中,在广播发出之后注册的广播接收器也能收到之前发送的该广播。


3、源码解析部分
3.1 广播接收器的动态注册

广播分为静态注册与动态注册。其中静态注册的广播在应用安装的时候由系统 PMS(PackageManagerService) 完成整个注册过程。

而动态注册调用 Context#registerReceiver(BroadcastReceiver receiver, IntentFilter filter) 实际上是由 ContextImpl 来实现的。

// ContextWrapper.java
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
	// mBase 实际为 ContextImpl
    return mBase.registerReceiver(receiver, filter);
}

调用 mBase.registerReceiver(receiver, filter) 最终会来到如下方法:

// ContextImpl.java

/**
 * 
 * @param receiver
 * @param userId:通过 ContextImpl#getUserId() 得到
 * @param filter
 * @param broadcastPermission:null
 * @param scheduler:null
 * @param context:通过 ContextImpl#getOuterContext() 得到,一般为对应的 activity 或者 service
 */
private Intent registerReceiverInternal(BroadcastReceiver receiver, int userId,
        IntentFilter filter, String broadcastPermission,
        Handler scheduler, Context context) {
    IIntentReceiver rd = null;
    if (receiver != null) {
    	// 1
        if (mPackageInfo != null && context != null) {
            if (scheduler == null) {
                scheduler = mMainThread.getHandler();
            }
            rd = mPackageInfo.getReceiverDispatcher(
                receiver, context, scheduler,
                mMainThread.getInstrumentation(), true);
        } else {
            if (scheduler == null) {
                scheduler = mMainThread.getHandler();
            }
            rd = new LoadedApk.ReceiverDispatcher(
                    receiver, context, scheduler, null, true).getIIntentReceiver();
        }
    }
    try {
    	// 2
        final Intent intent = ActivityManagerNative.getDefault().registerReceiver(
                mMainThread.getApplicationThread(), mBasePackageName,
                rd, filter, broadcastPermission, userId);
        if (intent != null) {
            intent.setExtrasClassLoader(getClassLoader());
            intent.prepareToEnterProcess();
        }
        return intent;
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

注释 1:

会先得到 ReceiverDispatcher 实例,该实例是根据 BroadcastReceiver 对象来获取的,如果对应于 BroadcastReceiver 对象的 ReceiverDispatcher 实例已经存在,则直接获取,否则会新生成一个 ReceiverDispatcher 实例。

在 ReceiverDispatcher 实例中,会持有对应的 BroadcastReceiver 对象的引用,同时还有一个 InnerReceiver 对象的引用。

// ReceiverDispatcher 构造方法
ReceiverDispatcher(BroadcastReceiver receiver, Context context,
        Handler activityThread, Instrumentation instrumentation,
        boolean registered) {
    if (activityThread == null) {
        throw new NullPointerException("Handler must not be null");
    }
    mIIntentReceiver = new InnerReceiver(this, !registered);
    mReceiver = receiver;
    mContext = context;
    mActivityThread = activityThread;
    mInstrumentation = instrumentation;
    mRegistered = registered;
    mLocation = new IntentReceiverLeaked(null);
    mLocation.fillInStackTrace();
}

InnerReceiver 对象继承自 IIntentReceiver.Stub,即实际上是一个 Binder 类型的对象。InnerReceiver 对象也会反过来持有 ReceiverDispatcher 实例对象的引用。

IIntentReceiver.Stub 实现自 IIntentReceiver 接口 。

注释 2:
获得对应的 InnerReceiver 实例之后,然后通过 AMS Binder 代理对象,在 AMS 进程中对广播接收器进行注册。

实际上,是对传递过去的 InnerReceiver 与 IntentFilter 保存起来。 此时广播接收器对象是在应用进程的,根本没有传递到 AMS 中,也没有直接保存到 AMS 中。

// ActivityManagerService.java
public Intent registerReceiver(IApplicationThread caller, String callerPackage,
        IIntentReceiver receiver, IntentFilter filter, String permission, int userId) {
    ...
    synchronized (this) {
        ...
        // 将 IIntentReceiver 对应的 ReceiverList 取出。
        // ReceiverList 持有着该 IIntentReceiver 对应 BroadcastFilter 元素列表,
        // 同时也会持有 receiver 的引用。
        // 每一个 BroadcastFilter 都由 IntentFilter 包装而成。
        ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder());
        if (rl == null) {
            rl = new ReceiverList(this, callerApp, callingPid, callingUid,
                    userId, receiver);
            if (rl.app != null) {
                rl.app.receivers.add(rl);
            } else {
                try {
                    receiver.asBinder().linkToDeath(rl, 0);
                } catch (RemoteException e) {
                    return sticky;
                }
                rl.linkedToDeath = true;
            }
            mRegisteredReceivers.put(receiver.asBinder(), rl);
        } else if (...) {
            ...
        }
        // BroadcastFilter 会持有 IIntentReceiver receiver 将对应的 ReceiverList rl
        BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage,
                permission, callingUid, userId);
        // 将这一次注册广播接收器时对应的 IntentFilter 对应的 BroadcastFilter 添加
        rl.add(bf);
        
        // 把 BroadcastFilter 添加到解析器中
        mReceiverResolver.addFilter(bf);
        ...
        return sticky;
    }
}

InnerReceiver 传递到 AMS 中,其主要作用是作为一个标识,其能够标识在应用进程中对应的广播接收器。当发送的广播在 AMS 中根据 intent-filter 匹配到了对应的 InnerReceiver(即 IIntentReceiver),之后会切换到对应的应用进程中的主线程,通过应用进程的 InnerReceiver 本地对象持有的 ReceiverDispatcher 对象,进一步得到对应的 BroadcastReceiver 对象,从而回调 BroadcastReceiver 对象的 onReceive() 方法。

3.2 普通广播的发送和接收

这里说的是普通广播的发送与接收,有序广播或者黏性广播的流程类似。

广播的发送和接收,本质上是一个过程的两个阶段。大致的流程如上所述。

通过 Context#sendBroadcast() 实际上也是由 ContextImpl 实现的。

// ContextImpl.java
public void sendBroadcast(Intent intent) {
    warnIfCallingFromSystemProcess();
    String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
    try {
        intent.prepareToLeaveProcess(this);
        ActivityManagerNative.getDefault().broadcastIntent(
                mMainThread.getApplicationThread(), intent, resolvedType, null,
                Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE, null, false, false,
                getUserId());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

会基于 Binder 机制切换到 AMS 进程,最终会进一步切换到 AMS#broadcastIntentLocked()

final int broadcastIntentLocked(ProcessRecord callerApp,
        String callerPackage, Intent intent, String resolvedType,
        IIntentReceiver resultTo, int resultCode, String resultData,
        Bundle resultExtras, String[] requiredPermissions, int appOp, Bundle bOptions,
        boolean ordered, boolean sticky, int callingPid, int callingUid, int userId) {
        
    intent = new Intent(intent);
    
    // By default broadcasts do not go to stopped apps.
    // 这里就是前面说的 FLAG_EXCLUDE_STOPPED_PACKAGES 标志
    intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES);
	...
    
    // registeredReceivers 用于收集匹配此次广播的 BroadcastFilter
    // BroadcastFilter 在前面的 AMS#registerReceiver() 中说过,BroadcastFilter 实例持有
    // ReceiverList 持有着 ReceiverList,而 ReceiverList 又会持有对应的 IIntentReceiver 引用
    List<BroadcastFilter> registeredReceivers = null;
    ...
    
    int NR = registeredReceivers != null ? registeredReceivers.size() : 0;
    if (!ordered && NR > 0) {
        // If we are not serializing this broadcast, then send the
        // registered receivers separately so they don't wait for the
        // components to be launched.
        if (isCallerSystem) {
            checkBroadcastFromSystem(intent, callerApp, callerPackage, callingUid,
                    isProtectedBroadcast, registeredReceivers);
        }
        // 根据 intent 得到 AMS 中的前台或者后台广播列表 BroadcastQueue
        // BroadcastQueue 持有着有序、无序广播列表
        final BroadcastQueue queue = broadcastQueueForIntent(intent);
        // 根据 intent、registeredReceivers 实例化对应的 BroadcastRecord 对象。
        // 其中 registeredReceivers 为 BroadcastFilter 列表
        BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp,
                callerPackage, callingPid, callingUid, resolvedType, requiredPermissions,
                appOp, brOptions, registeredReceivers, resultTo, resultCode, resultData,
                resultExtras, ordered, sticky, false, userId);
        if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing parallel broadcast " + r);
        final boolean replaced = replacePending && queue.replaceParallelBroadcastLocked(r);
        if (!replaced) {
            // 将 BroadcastRecord 对象添加到无序广播列表中
            queue.enqueueParallelBroadcastLocked(r);
            // 触发广播的发送
            queue.scheduleBroadcastsLocked();
        }
        registeredReceivers = null;
        NR = 0;
    }
    ...
    return ActivityManager.BROADCAST_SUCCESS;
}

AMS#broadcastIntentLocked() 中实际上会根据发送广播时设置的 intent-filter 来匹配出对应的 IIntentReceiver (一个或者数个),然后通过 queue.scheduleBroadcastsLocked() 来触发广播的发送。

(对于有序广播和黏性广播的逻辑也在上述方法中,且类似。)

// BroadcastQueue.java
public void scheduleBroadcastsLocked() {
    if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Schedule broadcasts ["
            + mQueueName + "]: current="
            + mBroadcastsScheduled);
    if (mBroadcastsScheduled) {
        return;
    }
    mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_INTENT_MSG, this));
    mBroadcastsScheduled = true;
}

然后通过 AMS 进程的 ServiceThread 线程的 handler 切换到 ServiceThread 线程中(之前处于 AMS Binder 线程池的线程中)。

之后进入到 BroadcastQueue#processNextBroadcast(true); 中:

final void processNextBroadcast(boolean fromMsg) {
    synchronized(mService) {
        BroadcastRecord r;
        mService.updateCpuStats();
        if (fromMsg) {
            mBroadcastsScheduled = false;
        }
        // 先将无序广播发出
        // First, deliver any non-serialized broadcasts right away.
        while (mParallelBroadcasts.size() > 0) {
            // 取得广播对应的 BroadcastRecord
            r = mParallelBroadcasts.remove(0);
            r.dispatchTime = SystemClock.uptimeMillis();
            r.dispatchClockTime = System.currentTimeMillis();
            final int N = r.receivers.size();
            // 遍历 BroadcastRecord 内部的 BroadcastFilter 列表
            // 每个 BroadcastFilter 元素会间接持有对应的 IIntentReceiver 对象,
            // IIntentReceiver 对象又对应着应用进程的广播接收器对象
            for (int i=0; i<N; i++) {
                Object target = r.receivers.get(i);
                // 进一步发送广播
                deliverToRegisteredReceiverLocked(r, (BroadcastFilter)target, false, i);
            }
            addBroadcastToHistoryLocked(r);
        }
        ...
	}
}

进一步通过 deliverToRegisteredReceiverLocked() 来发送广播:

// BroadcastQueue.java
private void deliverToRegisteredReceiverLocked(BroadcastRecord r,
        BroadcastFilter filter, boolean ordered, int index) {
    ...
    try {
        if (filter.receiverList.app != null && filter.receiverList.app.inFullBackup) {
            // Skip delivery if full backup in progress
            // If it's an ordered broadcast, we need to continue to the next receiver.
            if (ordered) {
                skipReceiverLocked(r);
            }
        } else {
            // 进一步调用 performReceiveLocked() 发送广播
            performReceiveLocked(filter.receiverList.app, filter.receiverList.receiver,
                    new Intent(r.intent), r.resultCode, r.resultData,
                    r.resultExtras, r.ordered, r.initialSticky, r.userId);
        }
        if (ordered) {
            r.state = BroadcastRecord.CALL_DONE_RECEIVE;
        }
    } catch (RemoteException e) {
        Slog.w(TAG, "Failure sending broadcast " + r.intent, e);
        if (ordered) {
            r.receiver = null;
            r.curFilter = null;
            filter.receiverList.curBroadcast = null;
            if (filter.receiverList.app != null) {
                filter.receiverList.app.curReceiver = null;
            }
        }
    }
}

接着又进一步来到 performReceiveLocked()

// BroadcastQueue.java
void performReceiveLocked(ProcessRecord app, IIntentReceiver receiver,
        Intent intent, int resultCode, String data, Bundle extras,
        boolean ordered, boolean sticky, int sendingUser) throws RemoteException {
    // Send the intent to the receiver asynchronously using one-way binder calls.
    if (app != null) {
        if (app.thread != null) {
            // If we have an app thread, do the call through that so it is
            // correctly ordered with other one-way calls.
            try {
                // 切换到应用进程的 ApplicationThread 中
                app.thread.scheduleRegisteredReceiver(receiver, intent, resultCode,
                        data, extras, ordered, sticky, sendingUser, app.repProcState);
            } catch (RemoteException ex) {
                // Failed to call into the process. It's either dying or wedged. Kill it gently.
                synchronized (mService) {
                    Slog.w(TAG, "Can't deliver broadcast to " + app.processName
                            + " (pid " + app.pid + "). Crashing it.");
                    app.scheduleCrash("can't deliver broadcast");
                }
                throw ex;
            }
        } else {
            // Application has died. Receiver doesn't exist.
            throw new RemoteException("app.thread must not be null");
        }
    } else {
        receiver.performReceive(intent, resultCode, data, extras, ordered,
                sticky, sendingUser);
    }
}

之后会切换到应用进程 ApplicationThread 中,并且会利用到 IIntentReceiver receiver。

// ActivityThread # ApplicationThread
public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
        int resultCode, String dataStr, Bundle extras, boolean ordered,
        boolean sticky, int sendingUser, int processState) throws RemoteException {
    updateProcessState(processState, false);
    receiver.performReceive(intent, resultCode, dataStr, extras, ordered,
            sticky, sendingUser);
}

会利用应用进程中本地的 Binder 对象,即 receiver 来触发对应的广播接收器对象的 onReceive() 方法。

前面说过,receiver (InnerReceiver)中会持有 LoadedApk.ReceiverDispatcher 对象,然后会进一步调用该对象的 performReceive() 方法。

// LoadedApk.ReceiverDispatcher
public void performReceive(Intent intent, int resultCode, String data,
        Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
    // Args 实现了 Runnable,其 run() 方法中会执行 ReceiverDispatcher 实例中保存的
    //  BroadcastReceiver 的 onReceive()
    final Args args = new Args(intent, resultCode, data, extras, ordered,
            sticky, sendingUser);
    if (intent == null) {
        Log.wtf(TAG, "Null intent received");
    } else {
        if (ActivityThread.DEBUG_BROADCAST) {
            int seq = intent.getIntExtra("seq", -1);
            Slog.i(ActivityThread.TAG, "Enqueueing broadcast " + intent.getAction()
                    + " seq=" + seq + " to " + mReceiver);
        }
    }
    // 通过 mActivityThread 发送到主线程中去执行 Args#run(),
    // 进而在主线程中的触发 BroadcastReceiver#onReceive()
    // 这里最终也会调用 IActivityManager#finishReceiver()
    if (intent == null || !mActivityThread.post(args)) {
        if (mRegistered && ordered) {
            IActivityManager mgr = ActivityManagerNative.getDefault();
            if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
                    "Finishing sync broadcast to " + mReceiver);
            args.sendFinished(mgr);
        }
    }
}

其中 Args 实现来 Runnable 接口,在其 run() 方法中会执行 ReceiverDispatcher 实例中保存的 BroadcastReceiver 的 onReceive()

当然,该 Args 是通过主线程的 handler 切换到主线程中去执行的。

// Args
public void run() {
    final BroadcastReceiver receiver = mReceiver;
    ...
    try {
        ClassLoader cl =  mReceiver.getClass().getClassLoader();
        intent.setExtrasClassLoader(cl);
        intent.prepareToEnterProcess();
        setExtrasClassLoader(cl);
        receiver.setPendingResult(this);
        // 最终目的地,调用 BroadcastReceiver#onReceive()
        receiver.onReceive(mContext, intent);
    } catch (Exception e) {
        ...
    }
    ...
}

关于普通广播的发送与接收过程的简单说明就大概如此。

3.3 广播接收器的后续处理

在上述流程中,会有多处地方涉及到 IActivityManager#finishReceiver() 的逻辑,如InnerRecriver#performReceive()ReceiverDispatcher#performReceive()、以及在 Args 中通过其 sendFinished() 方法间接调用,等等。

这些地方调用 IActivityManager#finishReceiver() 的时机,是在广播被广播接收器处理完之后,或者处理异常等情况。

而调用 IActivityManager#finishReceiver(),时机上会切换到 AMS 进程的 finishReceiver() 方法,在该方法中,又会进一步调用 trimApplications() 方法:

final void trimApplications() {
    synchronized (this) {
        int i;
        // First remove any unused application processes whose package
        // has been removed.
        for (i=mRemovedProcesses.size()-1; i>=0; i--) {
            final ProcessRecord app = mRemovedProcesses.get(i);
            if (app.activities.size() == 0
                    && app.curReceiver == null && app.services.size() == 0) {
                Slog.i(
                    TAG, "Exiting empty application process "
                    + app.toShortString() + " ("
                    + (app.thread != null ? app.thread.asBinder() : null)
                    + ")\n");
                if (app.pid > 0 && app.pid != MY_PID) {
                    app.kill("empty", false);
                } else {
                    try {
                        app.thread.scheduleExit();
                    } catch (Exception e) {
                        // Ignore exceptions.
                    }
                }
                cleanUpApplicationRecordLocked(app, false, true, -1, false /*replacingPid*/);
                mRemovedProcesses.remove(i);
                if (app.persistent) {
                    addAppLocked(app.info, false, null /* ABI override */);
                }
            }
        }
        // Now update the oom adj for all processes.
        updateOomAdjLocked();
    }
}

猜测该方法 trimApplications() 的作用是为了退出那些因接收广播而被启动的应用进程,它会判断这些进程是否还有 activity、service 在运行,否则的话,就会终止应用进程。

根据这个推测,可以进一步推测一个场景,那就是当某个广播触发了本身未运行的应用,在响应广播之后,该应用进程会被退出。

4. BroadcastReceiver,LocalBroadcastReceiver 区别

摘抄自:https://github.com/JsonChao/Awesome-Android-Interview/blob/master/Android%E7%9B%B8%E5%85%B3/Android%E5%9F%BA%E7%A1%80%E9%9D%A2%E8%AF%95%E9%A2%98.md#75broadcastreceiverlocalbroadcastreceiver-%E5%8C%BA%E5%88%AB

应用场景

1、BroadcastReceiver 用于应用之间的传递消息;

2、而LocalBroadcastManager 用于应用内部传递消息,比 BroadcastReceiver更加高效。

安全

1、BroadcastReceiver 使用的 Context API(原本说是 Content API,不是很理解,有懂的同学可以说名一下,这里感觉应该说的是 Context),本质上它是跨应用的,所以在使用它时必须要考虑到不要被别的应用滥用;

2、LocalBroadcastManager 不需要考虑安全问题,因为它只在应用内部有效。

原理方面

(1) 与BroadcastReceiver 是以 Binder 通讯方式为底层实现的机制不同,LocalBroadcastManager 的核心实现实际还是 Handler,只是利用到了 IntentFilter 的 match 功能,至于 BroadcastReceiver 换成其他接口也无所谓,顺便利用了现成的类和概念而已。

(2) LocalBroadcastManager 因为是 Handler 实现的应用内的通信,自然安全性更好,效率更高。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值