Android 远程调用服务之 AIDL

news2024/11/24 15:54:52

目录

一、AIDL 是什么?
二、为什么要使用 AIDL?
    1、使用 AIDL 是为了跨进程调用第三方服务?
    2、使用 AIDL 是为了向第三方服务传输数据/参数?
    3、使用 AIDL 是为了获取第三方服务直接或者异步返回的数据?
三、提供哪些文件给客户端?
    1、直接 copy 所有的 .aidl 文件给客户端,并要求客户端保持包结构;
    2、先编译 .aidl 文件,然后把 build 编译后生成的 .java 文件 copy 给客户端,并要求客户端保持包结构;
    3、把 .aidl 文件放在一个单独的 (Android Studio) Module 中,build 编译生成的 .aar 文件 copy 给客户端依赖;
四、AIDL 项目结构
    1、Server 独立工程,AIDL 独立工程,Client 独立工程。
    2、Server + AIDL 独立工程,Client 独立工程。(推荐)
五、案例:Client 跨进程调用 Server 端的 Service 服务,并获取异步处理数据的结果。
    一)、本案例项目结构:
    二)、AidlLib 接口编写:
        1、新建 AIDL Folder
        2、新建 .aidl 文件配置
        3、新建 AIDL 通讯接口文件 IMyTestAidlInterface.aidl
        4、新建 searchKeyWord() 方法中的参数回调接口 IMyTestCallback.aidl
        5、如果需要使用 Java Bean 数据类了,需要怎么处理呢?
        6、最后单独 aidl 代码的 Module 执行 build 编译,把生成的 .aar 文件 copy 给 客户端依赖。
    三)、Server 服务提供方:
        1、修改 Server 的 build.gradle 文件,源码依赖 aidl 的 Module
        2、新建 Service 类 MyService.java
        3、在清单文件中配置 Servcie
    四)、Client 业务调用方:
        1、把 .aar 文件 copy 到 libs 目录中,并配置依赖
        2、在界面上添加一个 TextView
        3、点击 TextView 时 bind 调用远端 Service
        4、创建上面 bindService() 需要的 intent 参数
        5、创建上面 bindService() 需要的 ServiceConnection 参数
        6、创建 callback 对象,提供给 searchKeyWord() 方法参数使用
        7、在清单文件中配置 <queries>
六、测试步骤和效果
    1、 安装 Servcie 类所在的应用
    2、安装客户端应用
    3、启动客户端应用
    4、点击客户端 Activity 的 TextView,异步获取服务端的内容
七、测试环境

​----------------------------------

 Demo 代码:

AIDL_Server_test:https://github.com/mengzhinan/AIDL_Server_test
AIDL_Client_test:https://github.com/mengzhinan/AIDL_Client_test
----------------------------------

正文内容

一、AIDL 是什么?

AIDL 英文全称:Android Interface Definition Language,中文含义:Android 接口定义语言

Android 接口定义语言 (AIDL) 官方文档:Android 接口定义语言 (AIDL)  |  Android 开发者  |  Android Developers

AIDL 是 Android 接口定义语言,是一种代码规范,需要通过编译后生成 .java 文件,然后提供给 Server 和 Client 使用,实现 Android 不同应用之间跨进程通讯。

二、为什么要使用 AIDL?

1、使用 AIDL 是为了跨进程调用第三方服务?

我觉得不一定。不使用 AIDL ,构造好 intent,直接调用 context.startService / context.bindService 调起第三方服务也可以通讯。

2、使用 AIDL 是为了向第三方服务传输数据/参数?

我觉得不一定。在使用 context.startService / context.bindService 方法调用服务时,可以在 intent 中携带参数。

3、使用 AIDL 是为了获取第三方服务直接或者异步返回的数据?

对,我认为是这个。要不然还需要在 .aidl 文件中编写接口方法干嘛,当然是想获取远端 Service 的处理结果,甚至是远端 Service 的异步处理结果。

三、提供哪些文件给客户端?

AIDL 只是一种接口定义语言规范,不是可执行的 Java 代码。编写好 .aidl 文件后,还需要 build 编译才会生成最终的 .java 文件。

那么,应该提供哪些文件给调用端(客户端)呢?

1、直接 copy 所有的 .aidl 文件给客户端,并要求客户端保持包结构;

2、先编译 .aidl 文件,然后把 build 编译后生成的 .java 文件 copy 给客户端,并要求客户端保持包结构;

3、把 .aidl 文件放在一个单独的 (Android Studio) Module 中,build 编译生成的 .aar 文件 copy 给客户端依赖;

四、AIDL 项目结构

从上面的信息了解到,可以以多种形式向客户端提供 .java 文件。同时发现,如果提供 .aar 包的话,连 .aidl 文件的包结构都不用关心。

.aidl 文件独立 Module 的方式也有两种,我更倾向于第二种:

1、Server 独立工程,AIDL 独立工程,Client 独立工程。

新建一个 AIDL 的工程,然后新建一个 Module,把所有的 .aidl 文件和必要的 .java 数据传输文件放在改 Module 中,build 编译产出 .aar 文件。

最后把 .aar 文件提供给 Server 和 Client 依赖使用。

测试时,只需要运行 Server 和 Client 的 app Module 即可,AIDL 工程的 app Module 不需要安装在测试机中。

2、Server + AIDL 独立工程,Client 独立工程。(推荐)

在 Server 工程中,新建一个 Module 专门用于存放某业务的 .aidl 文件,build 编译产出 .aar 文件。

Server 与 AIDL 之间直接使用 Module 方式源码依赖就可以了,Client 与 AIDL 之间还是使用 .aar 包方式依赖。

五、案例:Client 跨进程调用 Server 端的 Service 服务,并获取异步处理数据的结果。

一)、本案例项目结构:

二)、AidlLib 接口编写:

1、新建 AIDL Folder

aidl 目录下存放所有的 .aidl 文件,java 目录下存放所有的 .java 文件。

建议 aidl 和 java 目录的包名一致。如果坚决不想保持一致,那数据类 Java Bean 的包结构必须保持一致,比如: UserData.java 和 UserData.aidl 文件。

2、新建 .aidl 文件配置

如果 AIDL File 选项是灰色的不可点击,则需要在该 Module 下的 build.gradle 文件中添加配置 aidl = true,如:

android {
    。。。
 
    // 添加 aidl folder
    sourceSets {
        getByName("main") {
            aidl {
                srcDirs("src/main/aidl")
            }
        }
    }
 
    buildFeatures {
        // 如果 aidl 目录未变色/无法新建 aidl 文件,则需要配置这个
        aidl = true
    }
}
3、新建 AIDL 通讯接口文件 IMyTestAidlInterface.aidl

计划在客户端调用 searchKeyWord() 方法,并传入 IMyTestCallback 回调接口。

等到服务端处理完数据后,调用 IMyTestCallback 回调接口向客户端传回数据。

// IMyTestAidlInterface.aidl
package com.fffffff.aidllib;
 
// Declare any non-default types here with import statements
import com.fffffff.aidllib.IMyTestCallback;
 
interface IMyTestAidlInterface {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void searchKeyWord(int anInt, String aString, IMyTestCallback myCallback);
 
    int addNum(int a, int b);
}
4、新建 searchKeyWord() 方法中的参数回调接口 IMyTestCallback.aidl

等待远端 Server 的 Service 处理完数据后,主动调用该接口的 onXXX() 方法回传数据。

// IMyTestCallback.aidl
package com.fffffff.aidllib;
 
// Declare any non-default types here with import statements
import com.fffffff.aidllib.UserData;
 
interface IMyTestCallback {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void onResult(String msg, in UserData userData);
 
    void onFailure(String error);
}
5、如果需要使用 Java Bean 数据类了,需要怎么处理呢?

首先,在 java 目录下新建 Java 类 UserData.java ,然后实现序列化接口 Parcelable。

如果 UserData 参数设置了 out 或者 inout 标签的话,还需要实现如下方法:

//    /**
//     * 参数方向为 out or inout 时,才需要此方法
//     * 手动添加此方法
//     *
//     * @param in in
//     */
//    public void readFromParcel(Parcel in) {
//        percentage = in.readInt();
//        msg = in.readString();
//    }

更多关于 in、out 和 inout 标签的用法,请继续查阅资料,我也不太了解。

第二步,还需要在 aidl 目录下编写 UserData.java 的 UserData.aidl 描述文件

注意,文件中 parcelable 的 p 是小写的。

6、最后单独 aidl 代码的 Module 执行 build 编译,把生成的 .aar 文件 copy 给 客户端依赖。

三)、Server 服务提供方:

1、修改 Server 的 build.gradle 文件,源码依赖 aidl 的 Module

2、新建 Service 类 MyService.java

因为客户端想要获取服务端的计算结果,所以要使用 bindService 方式调用服务。

因此服务端需要实现 onBind(Intent intent)() 方法,并返回 binder 对象,使客户端能够调用到 aidl 定义的方法。

onBind(Intent intent)() 方法需要返回一个 IBinder 对象,刚好 IMyTestAidlInterface.Stub 抽象类实现了 IBinder 接口:

package com.fffffff.aidlserver;
 
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import androidx.annotation.Nullable;
import com.fffffff.aidllib.IMyTestAidlInterface;
import com.fffffff.aidllib.IMyTestCallback;
import com.fffffff.aidllib.UserData;
 
public class MyService extends Service {
 
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new IMyTestAidlInterface.Stub() {
            @Override
            public void searchKeyWord(int i, String s, IMyTestCallback iMyTestCallback) throws RemoteException {
                UserData userData = new UserData();
                userData.percentage = i;
                userData.msg = s;
                iMyTestCallback.onResult("服务端处理完毕", userData);
            }
 
            @Override
            public int addNum(int a, int b) throws RemoteException {
                return a + b;
            }
        };
    }
}

注意:

IMyTestAidlInterface.Stub.searchKeyWord() 方法没有直接返回值,而是使用异步的方式回传数据。

当数据处理完毕后,调用 IMyTestCallback 接口的 onResult() 方法回传数据。

3、在清单文件中配置 Servcie

注意配置:

是否启用此 Service 类:android:enabled="true" 

是否允许其他进程调用此服务:android:exported="true" 

自己定义一个 Action,不重复就行:<action android:name="com.fffffff.aidlserver.action.MY_SERVICE_CENTER" />

四)、Client 业务调用方:

1、把 .aar 文件 copy 到 libs 目录中,并配置依赖

2、在界面上添加一个 TextView

3、点击 TextView 时 bind 调用远端 Service
tvShow.setOnClickListener {
    tvShow.isClickable = false
    // 点击 TextView 绑定服务
    val intent = BindServerUtil.buildIntent3()
    this.applicationContext?.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
4、创建上面 bindService() 需要的 intent 参数

调用 servcie 的 intent 方式很多。

→ setComponent() 方式:

@JvmStatic
fun buildIntent1(): Intent {
    val intent = Intent()
    // setComponent() 方式
    intent.component = ComponentName("com.fffffff.aidl_server_test", "com.fffffff.aidlserver.MyService")
    return intent
}

→ setClassName() 方式:

@JvmStatic
fun buildIntent2(): Intent {
    val intent = Intent()
    // setClassName() 方式
    intent.setClassName("com.fffffff.aidl_server_test", "com.fffffff.aidlserver.MyService")
    return intent
}

→ setAction() 方式:

@JvmStatic
fun buildIntent3(): Intent {
    val intent = Intent()
    // setAction() 方式
    intent.action = "com.fffffff.aidlserver.action.MY_SERVICE_CENTER"
    intent.setPackage("com.fffffff.aidl_server_test")
    return intent
}

注意:

上面提到的包名为,数据提供方(服务端)的可执行 Module 的包名,比如常见的 App Module,不是 Service 类所在的子 Module 的包名。

上面提到的 Service 全类名为,Service 类实际所在的包名 + 类名称。

上面提到的 Action 为,数据提供方(服务端)中 Service 类清单文件中定义的 Action 值。

上面三种方法,推荐使用第三方方法 action 的方式。因为你不确定服务端以后会不会对代码重构,如果修改 Service 的类名或包名了呢。

5、创建上面 bindService() 需要的 ServiceConnection 参数

ServiceConnection 是一个接口,直接 new 一个实现类传入到 bindService() 方法中。

在 onServiceConnected() 抽象方法中,使用 IMyTestAidlInterface.Stub.asInterface(IBinder) 方法把 service: IBinder? 参数转换为 IMyTestAidlInterface 类型。即服务端 Service 中返回的 binder 对象。

// 服务绑定成功/失败的回调
private val connection: ServiceConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        tvShow.text = "连接成功"
        try {
            iMyTestAidlInterface = IMyTestAidlInterface.Stub.asInterface(service)
 
            val callback = createBinderCallback()
            iMyTestAidlInterface?.searchKeyWord(404, "wuhan", callback)
        } catch (e: Exception) {
            tvShow.text = "连接失败 error = ${e.message}"
            e.printStackTrace()
        }
    }
 
    override fun onServiceDisconnected(name: ComponentName?) {
        tvShow.text = "连接断开"
    }
}

有了服务端的 IMyTestAidlInterface 对象,就可以直接调用服务端实现的接口方法了。

此处调用 searchKeyWord() 方法,并传入回调接口,类似于网络请求的成功与失败的 callback 接口。

6、创建 callback 对象,提供给 searchKeyWord() 方法参数使用

小心,有坑,巨坑

searchKeyWord() 方法需要 IMyTestCallback 类型的对象作为参数,那么是直接 new 一个 IMyTestCallback 的实现类吗?不是啊,坑坑坑

而是需要 new 一个 IMyTestCallback.Stub 类型的实现类对象:

/**
 * 客户端的回调接口需要 new IMyTestCallback.Stub 的实现类,
 * 而不是 new IMyTestCallback 的实现类。
 */
private fun createBinderCallback(): IMyTestCallback.Stub {
    return object : IMyTestCallback.Stub() {
        override fun onResult(p0: String, p1: UserData?) {
            tvShow.text = "$p0:\n" + "UserData = $p1\nint = ${p1?.percentage}\nmsg = ${p1?.msg}"
        }
 
 
        override fun onFailure(p0: String?) {
            tvShow.text = "获取失败 = $p0"
        }
 
    }
}

最后,在回调 callback 中获取服务端 Service 返回的数据,设置到 TextView 中。

7、在清单文件中配置 <queries>

如果只是上面的内容及配置,在高版本的 Android 系统中运行是无法调起远端 Service 的,因为 Android 11 版本对手机已安装应用的包可见行做了限制。

如果不配置的话,则客户端应用无法发现服务端应用,导致无法调起。

配置如下:

包可见行定义有两种方式。

-> 定义 action 可见。

即只对服务端某个 Service 的 action 配置可见。配置好后,客户端就可以通过 action 隐私启动绑定服务端的 Service 了。

<!-- 定义某个 app 的某个 action 可见 -->
<intent>
    <action android:name="com.fffffff.aidlserver.MY_SERVICE_CENTER" />
</intent>

-> 定义应用的包名可见。

即针对某个应用的包名可见。

拿本例说,不是服务端的 aidl Module 的包名,也不是服务端的 Service 类所在的 Module 的包名。而是 Service 类所在的可运行的 Module,即 app Module 的包名。

<!-- 定义某个 app 应用的包名可见 -->
<package android:name="com.fffffff.aidl_server_test" />

六、测试步骤和效果

1、 安装 Servcie 类所在的应用

注意:杀死服务端 app,无需提前启动服务端

2、安装客户端应用

3、启动客户端应用

TextView 显示默认的内容

4、点击客户端 Activity 的 TextView,异步获取服务端的内容

客户端已经成功获取服务端返回的数据了。

>>>>>>>> 完毕 <<<<<<<

七、测试环境

当前测试环境的详情如下:

Android Studio 版本:Android Stuido Giraffe | 2022.3.1 Patch 1

Gradle::8.0

JDK:17

TargetSDK:33

模拟器版本:Pixel 7 Pro API 33

tip:

一开始不要使用真机测试,尤其是定制化的手机,比如小米/华为等。我的 Demo 在小米/华为手机上,一直无法调起服务端 Service,网上查阅资料说是手机系统做限制了,禁止关联启动。

使用 Google 模拟器测试,就可以成功调起。

Demo 代码:

AIDL_Server_test:https://github.com/mengzhinan/AIDL_Server_test
AIDL_Client_test:https://github.com/mengzhinan/AIDL_Client_test

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1100516.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux性能优化--性能工具:磁盘I/O

6.0 概述 本章介绍的性能工具能帮助你评估磁盘I/O子系统的使用情况。这些工具可以展示哪些磁盘或分区已被使用&#xff0c;每个磁盘处理了多少I/O,发给这些磁盘的I/O请求要等多久才被处理。 阅读本章后&#xff0c;你将能够&#xff1a; 确定系统内磁盘I/O的总量和类型(读/写…

浅谈智能照明控制系统应用在城市轨道交通

叶根胜 江苏安科瑞电器制造有限公司 江苏江阴 214405 摘要&#xff1a;在传统的城市轨道交通设计方面&#xff0c;照明设计方案具有一定的弊端。随着计算机技术的发展&#xff0c;智能化技术渐渐步入人们的生活并成为主流&#xff0c;故在城市轨道交通中应用新型的照明控制设…

基于关联规则的多样化推荐技术应用研究

摘要 目录 第二章 相关理论介绍 2.2.1 当前常用推荐技术 关联规则推荐 2.3.1 关联规则相关概念

【java学习—七】Object类(36)

文章目录 1. 概念2. Object类中的主要方法 1. 概念 Object 类是所有 Java 类的根父类如果在类的声明中未使用 extends 关键字指明其父类&#xff0c;则默认父类为Object 类 public class Person {...}等价于&#xff1a;public class Person extends Object {...}例&#xff1…

新加坡服务器托管

新加坡是一个小而繁荣的国家&#xff0c;是东南亚唯一一个发达国家。它地理位置好&#xff0c;毗邻马来西亚和印度尼西亚&#xff0c;新加坡是一个拥有先进科技和强大经济的国家&#xff0c;主要以制造业、金融、旅游和航运为主&#xff0c;拥有先进的经济和现代化的基础设施&a…

计算机操作系统-第九天

1、虚拟机 传统计算机的特点&#xff1a;一台物理机器只能运行一个操作系统 虚拟机的特点&#xff1a; 使用虚拟化技术&#xff0c;将一台物理机器虚拟化为多台虚拟机器&#xff08;Virtual Machine&#xff0c;简称VM&#xff09;每个虚拟机都可以独立运行一个操作系统 虚拟…

大数据学习(12)-join优化common join

&&大数据学习&& &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 承认自己的无知&#xff0c;乃是开启智慧的大门 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一下博>主哦&#x…

数据结构知识点总结

一、常见的数据结构 数组&#xff0c;栈&#xff0c;队列&#xff0c;链表&#xff0c;散列表&#xff0c;二叉树&#xff0c;堆&#xff0c;跳表&#xff0c;图&#xff0c;树。 1. 数组&#xff1a; 数组的元素在内存中存储是连续存放的&#xff0c;占有连续的存储单元&am…

AWS S3加密

Hello大家好&#xff61; 在本课时我们将讨论S3加密相关的内容。 S3加密相关是认证考试的一个重要的主题考点&#xff0c;您需要了解亚马逊S3的几种不同类型的加密方式。| 首先是静态数据的加密&#xff0c;静态数据加密是指数据存储在亚马逊S3 数据中心的磁盘上时&#xff0…

Excel文档名批量翻译不求人

在我们的日常工作中&#xff0c;经常需要处理大量的文件&#xff0c;包括Excel文档。有时候&#xff0c;我们需要对文件名进行修改或者翻译&#xff0c;以便更有效地进行文件管理和数据统计。那么&#xff0c;如何高效地翻译Excel文档名呢&#xff1f;下面是一个实用的方法。 首…

感测型离子风机在线实时监测

感测型离子风机在线实时监测静电是指通过安装静电监测器和数据采集设备&#xff0c;对离子风机所处环境的静电情况进行实时监测和数据采集&#xff0c;以便及时发现并解决静电问题。 感测型离子风机在线实时监测静电可以实现以下功能&#xff1a; 实时监测离子风机所处环境的静…

【FreeRTOS】【STM32】06.1 FreeRTOS的使用1(对06的补充)

前后台系统(裸机) 裸机又称前后台系统&#xff0c;在一个while中不停循环处理各个task。 中断服务函数作为前台程序 大循环while(1)作为后台程序 多任务系统 通过任务调度的方式&#xff0c;执行各个任务&#xff0c;优先级高的先执行&#xff0c;执行完了释放CPU使用权&am…

智慧工地管理系统(Smart site management system)源码

智慧工地管理系统是面向建筑工程施工企业提供的工地智能交互平台&#xff0c;基于云计算、物联网、人工智能等技术的应用&#xff0c;对现场人员、材料、机械、质量、安全等进行高效管控&#xff0c;以实现项目管理人员对现场的实时把控、及时预警、精准决策&#xff0c;从而为…

Golang学习:基础篇练习(二)—— 数组切片小实验

Golang学习&#xff1a;基础篇练习&#xff08;二&#xff09;—— 数组切片小实验 本次实验是对数组切片的&#xff0c;目的就是为了加深理解&#xff0c;一起来看看吧。 1、下面这段代码有问题吗&#xff1f;为什么 func main() {greeting : make([]string, 3, 5)// 3 is len…

FPGA基于1G/2.5G Ethernet PCS/PMA or SGMII实现 UDP 网络视频传输,提供工程和QT上位机源码加技术支持

目录 1、前言版本更新说明免责声明 2、我这里已有的以太网方案3、设计思路框架视频源选择OV5640摄像头配置及采集动态彩条UDP协议栈UDP视频数据组包UDP协议栈数据发送UDP协议栈数据缓冲IP地址、端口号的修改Tri Mode Ethernet MAC1G/2.5G Ethernet PCS/PMA or SGMIIQT上位机和源…

08-网络篇-一步步完成网络通信

上面的章节描述了Tcp/ip四层网络模型&#xff0c;并介绍了每一层的常见的协议&#xff0c;本章来讨论一下&#xff0c;一个主机是怎样与外网中的另一个主机通信。 假如网络结构如下 上图是一个典型的网络结构图&#xff0c;若主机1是电脑&#xff0c;主机4是服务器&#xff0c;…

申请者用Chat GPT写申请文书?国外大学纷纷采取措施

近来&#xff0c;人工智能聊天机器人ChatGPT实火。ChatGPT拥有强大的信息整合能力、自然语言处理能力&#xff0c;可谓是“上知天文&#xff0c;下知地理”&#xff0c;而且还能根据要求进行聊天、撰写文章等。 ChatGPT一经推出&#xff0c;便迅速在社交媒体上走红&#xff0c…

NTP时间同步协议

NTP net working protocol网络时间同步协议概述 同步互联网中主机和路由器之间的时钟在局域网上提供亚毫秒精度&#xff0c;在广域网上提供几十毫秒精度冗余服务器和多种网络路径保证了可靠性工程算法用于减少抖动&#xff0c;缓解多个源和避免不正确操作的服务器 How NTP wo…

百度上怎么录视频?全攻略来了,一看就会!

在这个信息化高度发达的时代&#xff0c;通过网络平台录制视频已经成为许多人生活中不可或缺的一部分。无论是展示自己的工作成果、分享自己的生活点滴&#xff0c;还是传播知识与信息&#xff0c;录制视频都是一个非常受欢迎的方式。而百度作为全球最大的中文搜索引擎&#xf…

智能门锁产品中应用的抗干扰低功耗触摸感应芯片

智能门锁是指区别于传统机械锁的基础上改进的&#xff0c;在用户安全性、识别、管理性方面更加智能化简便化的锁具。智能门锁是门禁系统中锁门的执行部件。智能门锁区别于传统机械锁, 是具有安全性, 便利性, 先进技术的复合型锁具。使用非机械钥匙作为用户识别ID的成熟技术。 …