Android IPC 之 AIDL 跨进程通信

IPC 简介

IPC 的含义为进程间通信或者跨进程通信,是指两个进程之间数据交换的过程
多进程的情况分为两种:
1,一个应用因为某些原因自身需要采用多进程模式来实现
2,当前应用需要向其他应用获取数据

Android 中的多进程模式
正常情况下,在Android中多进程是指一个应用中存在多个进程的情况

在android 中使用多进程的方法:
1,给四大组件在AndroidMenifest中指定android:process属性(我们无法给一个线程或一个实体类指定其运行时所在的进程)
2,非常规方法:通过JNI在native层去fork一个新的进程

第一种情况:默认进程的名子为程序包名。如果需要指定,有两种方式。
android:process=":remote"
android:process=“com.ryg.chapter_2.remote”

两种区别:
第一种 “:”的含义是在当前进程名前附加上当前的包名,是一种简洁的写法
第二种 是完整的命名方式

第一种 是私有进程,其他应用组件不可以和它跑在同一个进程中
第二种 是全局进程,其他应用可以通过 ShareUID 的方式和他跑在同一个进程中。

Android系统会为每一个应用分配一个 UID ,具有相同UID 的应用才能共享数据,两个应用通过ShareUID 跑在同一个进程中是有要求的。需要这两个应用具有相同的ShareUID 并且签名相同才可以,在这种情况下,他们可以互相访问对方的私有数据,比如data目录,组件信息等,不管他们是否跑在同一个进程中。如果过他们跑在同一进程中,还可以共享内存数据,他们看起来就像是一个应用的两个部分。

android 系统会为每个进程分配一个独立的虚拟机,不同地虚拟机在内存分配上有不同的地址空间,所以早期不同的虚拟机中访问同一个类的对象会产生多个副本。

使用多进程容易造成一下几个问题

1,静态成员和单例完全失效

2,线程同步机制完全失效:无论锁对象还是全局对象。

3,SharedPrefrences 可靠性下降

4,Application 会多次创建

在这里插入图片描述

​ 如上面图片,启动多个进程后,Application 会多次执行,而且 as 上面显示有三个进程,通过试验,两个进程之间的静态数据不会被干扰,相当于把原来进程中的数据重新拷贝了一份。修改当前进程,别的进程数据也不会改变,尽管他是静态属性。


IPC 基础,主要包含三个方面:

1,Serializable 接口

​ java 提供的序列化接口。可以为对象提供标准的 序列化和反序列化。java 序列化机制和自定义序列化

2,Parcelable 接口

​ Android 提供过的序列化接口,只要实现这个接口,类对象就可以通过 Intent 和 Binder 传递。

class Book(userId: Int, bookName: String) : Parcelable {

    var id: Int = userId

    var bookName: String? = bookName


    /**
     * 将当前对象写入序列化结构中,通过一系列的 write 方法完成
     * 其中 flags 有两种值:0 或 1,
     * <p> 为 1 时标识当前对象需要作为返回值返回,不能立即释放资源
     * 几乎所有情况都返回 0
     */
    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeInt(id)
        parcel.writeString(bookName)
    }

    /**
     * 返回当前对象的描述,如果含有文件扫描符,返回1,否则返回0
     * 几乎所有情况都返回 0
     */
    override fun describeContents(): Int {
        return 0
    }

    /**
     * 反序列化
     */
    companion object CREATOR : Parcelable.Creator<Book> {

        /**
         * 从序列化后对象中创建原始对象
         */
        override fun createFromParcel(source: Parcel): Book {
            return Book(source.readInt(),source.readString()!!)
        }
        override fun newArray(size: Int): Array<Book?> {
            /**
             * 创建指定长度的原始对象数组
             */
            return arrayOfNulls(size)
        }
    }
}

3,Binder

​ 直观上来说:Binder 是Android 中的一个类,实现了 IBinder 接口。

​ 从 IPC 角度来说:Binder 是 Android 中一种跨进程通信的方式。

​ Binder 还可以理解为一种虚拟的物理设备,他的设备驱动是 /dev/binder。该通信方式在 Linux 上没有

​ 从 AndroidFramework 角度来说:Binder 是 ServiceManager 连接各种 Manager(ActivityManager,WindowManager,等等) 和相应 ManagerService 的桥梁

​ 从 Android 应用层来说:Binder 是客户端 和 服务端进行通信的媒介,当 binderService 的时候,服务端会返回一个包含了服务端业务调用的 Binder 对象,通过这个 Binder 对象,客户端就可以获取服务器提供的服务或者数据,这里的服务包括普通服务和基于 ALDL 的服务。

Binder 架构

在这里插入图片描述

​ 首先是 AIDL ,在 android 中通过编写 AIDL 去基于 Binder 去提供对外的接口和实现,然后通过 AIDL 文件调用到 对应的java 层,然后通过 java 的 JNI 调用到 Native 层,最后是调用到 内核

使用 AIDL

​ 使用 AIDL 进行进程间通信的流程,分为客户端和服务端两个方面

1,服务端

​ 服务端首先要创建一个 Service 用来监听客户端的连接请求,创建创建 AIDL 文件,将暴露给客户端的接口在这个 AIDL 文件中声明,最后在 Sercie 中实现这个接口

2,客户端

​ 首先绑定服务,将服务端的返回的 Binder 转成 AIDL 接口所属的类型,接着就可以调用 AIDL 中的方法了

注意事项

  • AIDL 是定义 IPC 过程中接口的一种描述语言
  • AIDL 文件在编译过程中生成接口的实现类,用于 IPC 通信
  • 支持基本数据类型,实现了 Parcelable 接口的对象,List,Map
  • 注意导包,使用到的 Parcelable 对象和 AIDL 对象必须显示的 import 进来,不管是否在同一个包下
  • 如果 AIDL 文件中用到了自定义的 Parcelable 对象,那么必须创建一个和他同名的 AIDL 文件,并声明拓为 Parcelable 类型。下面会后文件结构图

实战:通过 AIDL 实现跨进程通信

解决问题:

  • AIDL 如何实现 IPC
  • in ,out,inout 关键字的作用
  • oneway 关键字的作用
  • AIDL 如何实现 callback

项目场景

在子进程中有一个链接服务 和 消息服务

  • 连接服务:connect (建连),disconnect(断连),isConnected(链接状态)

  • 消息服务:sendMessage(发送消息),registerMessaageReceiverListener(在主进程中监听子进程的消息),unRegisterMessageReceiverListener(注销消息的监听)

1,创建子进程的 Service

/**
 * 管理和提供子进程的连接和消息服务
 */
class RemoteService : Service() {
    override fun onBind(intent: Intent?): IBinder? {
        return null
    }
}

在清单文件中进行注册,并指定在私有的子线程

<!--  子进程的Service  -->
<service
    android:name=".RemoteService"
    android:process=":remote" />

接着在 MainActivity 中启动这个服务

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //启动子进程的 Service
        bindService(Intent(this, RemoteService::class.java), object : ServiceConnection {
            override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            }

            override fun onServiceDisconnected(name: ComponentName?) {
            }

        }, Context.BIND_AUTO_CREATE)
    }
}

运行程序后就会发现子进程已经启动了

2,完成连接服务

​ 右击 module ,选择 new 中的 AIDL ,创建一个名字为 IConnectionService.aidl 的文件,并定义三个方法,也就是上面我们所说的三个方法,如下所示:

/**
 * 连接服务
 */
interface IConnectionService {

    void connect();

    void disconnect();

    boolean isConnected();
}

有几点需要注意一下:

​ 文件创建完成后,你就会发现他会自动在 main 文件夹下创建 aidl 文件夹,并且里面和 java 下的包名是一样的。

​ 这个接口中不能有任何注释,否则会编译不通过,切记

​ 点一下锤子,进行编译,看一下生成的实现类和目录结构:

在这里插入图片描述

接着我们来完成一下 处于子进程中的 RemoteService

/**
 * 管理和提供子进程的连接和消息服务
 */
class RemoteService : Service() {

    /**
     * 连接状态
     */
    private var isConnected: Boolean = false
    
    /**
     * 这三个方法就是 aidl 文件中的三个方法,在这里进行实现
     */
    val connectionService =
        object : IConnectionService.Stub() {
            //当主进程调用 connect 时,这里个 connect 就会执行
            override fun connect() {
                //模拟连接耗时
                Thread.sleep(5000)
                this@RemoteService.isConnected = true
                Log.e(  "connect","${android.os.Process.myPid()}   ${Thread.currentThread().name}")
                GlobalScope.launch(Dispatchers.Main) {
                    Toast.makeText(this@RemoteService, "connect", Toast.LENGTH_LONG).show()
                }
            }

            //当主进程调用 disconnect 时,这里个 disconnect 就会执行
            override fun disconnect() {
                this@RemoteService.isConnected = false
                Log.e("connect","${android.os.Process.myPid()}  --- ${Thread.currentThread().name}")
                //切换到主线程
                GlobalScope.launch(Dispatchers.Main) {
                    Toast.makeText(this@RemoteService, "disconnect", Toast.LENGTH_LONG).show()
                }
            }

            override fun isConnected(): Boolean {
                return this@RemoteService.isConnected
            }

        }

    override fun onBind(intent: Intent?): IBinder? {
        return connectionService.asBinder()
    }
}

​ 我们实现了 AIDL 实现类中的抽象类,并且实现了三个方法,这三个方法就是我们在接口中定义的方法。

​ 接着在 onBind 中 将他转为了 Binder 进行返回,这样在主进程中就可以进行调用了。如下

修改 MainActivity :

class MainActivity : AppCompatActivity() {

    private var connectionServiceProxy: IConnectionService? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //启动子进程的 Service
        initService()
        
        //三个 button 
        connect.setOnClickListener {
            connectionServiceProxy?.connect()
        }
        dis_connect.setOnClickListener {
            connectionServiceProxy?.disconnect()
        }
        is_connect.setOnClickListener {
            val isconnected = connectionServiceProxy?.isConnected
            Toast.makeText(this, "$isconnected", Toast.LENGTH_LONG).show()
        }
    }

    private fun initService() {
        bindService(Intent(this, RemoteService::class.java), object : ServiceConnection {
            override fun onServiceConnected(name: ComponentName?, service: IBinder) {
                //拿到 子进程中 onBind 返回的值
                connectionServiceProxy = IConnectionService.Stub.asInterface(service)
            }

            override fun onServiceDisconnected(name: ComponentName?) {
                 //销毁时调用,可用于重新连接
                Toast.makeText(this@MainActivity, "$name 销毁了", Toast.LENGTH_LONG).show()
            }

        }, Context.BIND_AUTO_CREATE)
    }
}

​ 首先是启动了 RemoteService,然后拿到了 onBinde 中返回的值。

​ 接着在 点击事件中进行了调用,观察打印的日志,如下:

 3741-3764/com.www.mk_ipc_demo:remote E/connect: 3741  --- Binder:3741_2
 3741-3764/com.www.mk_ipc_demo:remote E/connect: 3741  --- Binder:3741_2

​ 对比进程 id,可以看出是执行在 子进程中,并且还是执行在子线程中,正因为如此,所以上面在 Toast 的时候转到了主线程

​ 不知道你有没有注意到,当你点击连接的按钮后,按钮的状态在 5秒后才会恢复,如果没注意到赶紧去看一下。这是因为 connect 方法时阻塞的,只有等到子进程将这个方法执行完成后 主进程中的主线程才会继续往下执行。

​ 那这种情况有没有什么办法解决呢?如下

  • 直接在子线程中调用 connect 方法即可。

  • 使用 oneway 关键字,如下:

    interface IConnectionService {
       oneway void connect();
       .......
    }
    

    然后重写跑一些代码,就会发现按钮的状态直接恢复,不会在进行阻塞了。

    有一点需要注意:使用 oneway 后,该方法不能有任何的返回值,否则会报错

通过上面的代码,我们就实现了通过 AIDL 实现了连接服务,在主进程和子进程之间的 IPC 调用。

3,完成消息的发送,注册,反注册

  • 1,首先新建一个消息的实体类和消息实体类的 AIDL ,在 java 包下新建一个包 entity ,并新建一个 Message 消息对象。

    package com.www.mk_ipc_demo.entity
    
    import android.os.Parcel
    import android.os.Parcelable
    
    class Message() : Parcelable {
        /**
         * 消息内容
         */
        var content: String? = null
        /**
         * 消息的状态
         */
        var isSendSuccess = false
    
        constructor(parcel: Parcel) : this() {
            content = parcel.readString()
            isSendSuccess = parcel.readByte().toInt() != 0
        }
    
        override fun writeToParcel(parcel: Parcel, flags: Int) {
            parcel.writeString(content)
            parcel.writeByte(if (isSendSuccess) 1.toByte() else 0.toByte())
        }
    
        override fun describeContents(): Int {
            return 0
        }
    
        companion object CREATOR : Parcelable.Creator<Message> {
            override fun createFromParcel(parcel: Parcel): Message {
                return Message(parcel)
            }
    
            override fun newArray(size: Int): Array<Message?> {
                return arrayOfNulls(size)
            }
        }
    }
    

    接着在 aidl 的包下也新建一个 entity 的包,并建立一个 Message 的实体类,内容如下:

    package com.www.mk_ipc_demo.entity;
    parcelable Message;
    

    如果该文件没有在 entity 下,请移动到 entity目录下面,并且修改包名

    通过上面这句话,就会和 java 包下的Message 实体类相互关联。注意,这两个 Message 的路径一定要相同

  • 2,创建消息的事件接口,注意是 aidl 文件

    package com.www.mk_ipc_demo;
    import com.www.mk_ipc_demo.entity.Message;
    
    /**
     * 消息事件
     */
    interface MessageReceiverListener {
        void onReceiveMessage(in Message message);
    }
    

    注意:引用实体类 Message 必须加上 in 关键字

  • 3,创建消息服务

    package com.www.mk_ipc_demo;
    
    import com.www.mk_ipc_demo.entity.Message;
    import com.www.mk_ipc_demo.MessageReceiverListener;
    
    /**
     * 消息服务
     */
    interface ImessageService {
    
        void sendMessage(in Message message);
    
        void registerMessageReceiveListener(MessageReceiverListener listener);
    
        void unRegisterMessageRececleListener(MessageReceiverListener listener);
    
    }
    

    用于发送消息,注册消息事件,和取消注册

  • 4,创建服务的管理者,用于返回指定服务的 Binder

    interface IServiceManager {
        IBinder getService(String serviceName);
    }
    

    用于返回指定的 Ibinder

  • 5,最终的目录结构如下:

在这里插入图片描述

  • 6,接着就是在子进程的服务中使用了,如下:
class RemoteService : Service() {

    /**
     * 连接状态
     */
    private var isConnected: Boolean = false

    //注册消息监听的集合
    private val messageReceiverListener = arrayListOf<MessageReceiverListener>()

    private var scheduledThreadPoolExecutor: ScheduledThreadPoolExecutor? = null

    private var scheduledFuture: ScheduledFuture<*>? = null

    /**
     * Stub 是实现类中的抽象类
     * 这三个方法就是 aidl 文件中的三个方法,在这里进行实现
     */
    val connectionService =
        object : IConnectionService.Stub() {
            //当主进程调用 connect 时,这里个 connect 就会执行
            override fun connect() {
                //模拟连接耗时
                Thread.sleep(5000)
                this@RemoteService.isConnected = true
                GlobalScope.launch(Dispatchers.Main) {
                    Toast.makeText(this@RemoteService, "connect", Toast.LENGTH_LONG).show()
                }
                //每隔 5 秒钟,如有监听,则发送消息
                scheduledFuture =
                    scheduledThreadPoolExecutor?.scheduleAtFixedRate(object : Runnable {
                        override fun run() {
                            messageReceiverListener.forEach {
                                val message = Message()
                                message.content = "我是子进程发送的消息"
                                it.onReceiveMessage(message)
                            }
                        }
                    }, 5000, 5000, TimeUnit.MILLISECONDS)
            }

            //当主进程调用 disconnect 时,这里个 disconnect 就会执行
            override fun disconnect() {
                this@RemoteService.isConnected = false
                GlobalScope.launch(Dispatchers.Main) {
                    Toast.makeText(this@RemoteService, "disconnect", Toast.LENGTH_LONG).show()
                }
                scheduledFuture?.cancel(true)
            }

            override fun isConnected(): Boolean {
                return this@RemoteService.isConnected
            }

        }

    /**
     * 消息服务的实现
     */
    val messageService = object : ImessageService.Stub() {
        override fun sendMessage(message: Message?) {
            GlobalScope.launch(Dispatchers.Main) {
                Toast.makeText( this@RemoteService,  "${android.os.Process.myPid()} -- ${message?.content}",Toast.LENGTH_LONG).show()
            }
            //设置消息状态
            message?.isSendSuccess = isConnected
        }

        override fun registerMessageReceiveListener(listener: MessageReceiverListener?) {
            if (listener != null) messageReceiverListener.add(listener)
        }

        override fun unRegisterMessageRececleListener(listener: MessageReceiverListener?) {
            if (listener != null) messageReceiverListener.remove(listener)
        }

    }

    /**
     * Service 管理,可让主进程选择性的调用连接服务或者消息服务
     */
    private val serviceManager = object : IServiceManager.Stub() {
        override fun getService(serviceName: String?): IBinder? {
            if (IConnectionService::class.java.simpleName == serviceName) {
                return connectionService.asBinder()
            } else if (ImessageService::class.java.simpleName == serviceName) {
                return messageService.asBinder()
            } else
                return null
        }
    }


    override fun onBind(intent: Intent?): IBinder? {
        return serviceManager.asBinder()
    }

    override fun onCreate() {
        super.onCreate()
        scheduledThreadPoolExecutor = ScheduledThreadPoolExecutor(1)
    }
}

仔细看的话其实和之前的连接服务差不多。

在连接服务的下面完成了消息服务的实现。其中如果有消息就弹出对话框显示当前进程 id 和消息内容,如果调用了添加监听就将监听添加到集合,否则 remove 监听。

消息服务的下面 则是 服务的管理,通过传入的 name ,返回指定的 binder

接着在 onBInd 方法中 返回了 serviceManager 的 binder

最后看一下连接服务,连接成功后就会每隔5秒遍历一次监听,如果有监听,则发送消息。

  • 7,在主进程中调用

    class MainActivity : AppCompatActivity() {
    
        //连接服务
        private var connectionServiceProxy: IConnectionService? = null
        //消息服务
        private var messageServiceProxy: ImessageService? = null
        //管理Service
        private var serviceManagerProxy: IServiceManager? = null
    
        //注册消息事件监听
        //这个监听也是一个 AIDL 的实现,和在 RemoteService 中的实现是一样的,只不过是一个反向的
        //消息服务注册该事件后,就可在子进程中进行调用了
        private val messageReceiveListener = object : MessageReceiverListener.Stub() {
            override fun onReceiveMessage(message: Message?) {
                GlobalScope.launch(Dispatchers.Main) {
                    Toast.makeText(
                        this@MainActivity,
                        "${android.os.Process.myPid()} -- ${message?.content}",
                        Toast.LENGTH_LONG
                    ).show()
                }
            }
    
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            //启动子进程的 Service
            initService()
    
            //三个 button,用于连接服务
            connect.setOnClickListener {
                //这里也会阻塞50000
                connectionServiceProxy?.connect()
            }
            dis_connect.setOnClickListener {
                connectionServiceProxy?.disconnect()
            }
            is_connect.setOnClickListener {
                val isconnected = connectionServiceProxy?.isConnected
                Toast.makeText(this, "$isconnected", Toast.LENGTH_LONG).show()
            }
    
            //三个 button,用户消息服务
            send_message.setOnClickListener {
                val message = Message()
                message.content = "主进程发送的消息"
                messageServiceProxy?.sendMessage(message)
            }
            register_listener.setOnClickListener {
                //添加消息监听
       messageServiceProxy?.registerMessageReceiveListener(messageReceiveListener)
            }
            unregister_listener.setOnClickListener {
                //移除消息监听
       messageServiceProxy?.unRegisterMessageRececleListener(messageReceiveListener)
            }
        }
    
        private fun initService() {
            bindService(Intent(this, RemoteService::class.java), object : ServiceConnection {
                override fun onServiceConnected(name: ComponentName?, service: IBinder) {
                    serviceManagerProxy = IServiceManager.Stub.asInterface(service)
                    //获取连接服务
                    connectionServiceProxy = IConnectionService.Stub.asInterface(
           serviceManagerProxy?.getService(IConnectionService::class.java.simpleName)
                    )
                    //获取消息服务
                    messageServiceProxy = ImessageService.Stub.asInterface(
       serviceManagerProxy?.getService(ImessageService::class.java.simpleName)
                    )
                }
    
                override fun onServiceDisconnected(name: ComponentName?) {
                     //销毁时调用,可用于重新连接
                    Toast.makeText(this@MainActivity, "$name 销毁了", Toast.LENGTH_LONG).show()
                }
    
            }, Context.BIND_AUTO_CREATE)
        }
    }
    

    其实和原来的也差不多,在启动服务后获取消息服务的 IPC 连接 和 连接服务的 IPC 连接

    可以看到新增了三个 button,用于发送消息,添加消息和移除消息。

    然后还实现了一个消息的监听,用于在子进程中调用。

  • 8,结果

    运行程序,点击 connect ,五秒后连接成功。接着发送消息,子进程就会收到消息。

    点击添加监听后,这个每隔5秒监听就会被回调。并且打印当前进程的 id。

    但是点击取消后没有任何反应,这是为啥呢?

    ​ 因为在取消监听的时候传到子进程的对象会经过反序列化,所以取消时传入的对象和添加的对象已经不是同一个了。所以无法取消。

    解决:

    private val messageRemoteListener = RemoteCallbackList<MessageReceiverListener>()
    
    //每隔 5 秒钟,如有监听,则发送消息
                    scheduledFuture =
                        scheduledThreadPoolExecutor?.scheduleAtFixedRate(object : Runnable {
                            override fun run() {
                                val size = messageRemoteListener.beginBroadcast()
                                for (i in 0 until size) {
                                    val message = Message()
                                    message.content = "我是子进程发送的消息"
                                    messageRemoteListener.getBroadcastItem(i).onReceiveMessage(message)
                                }
                                messageRemoteListener.finishBroadcast()
                            }
                        }, 5000, 5000, TimeUnit.MILLISECONDS)
                        
    
    if (listener != null) messageRemoteListener.register(listener)
    if (listener != null) messageRemoteListener.unregister(listener)
                        
    

    使用系统提供的 RemoteCallbackList 即可。至于为什么可以,去看一下源码就明白了

关键字的作用

  • in :如果对象被标记了 in ,该对象被传到子进程中后,修改该对象的数据,主进程中的该对象不会发生任何改变。

  • inout:使用 inout 后,子进程的数据修改后,主进程的数据也会被更新。运行程序就会发现报了如下错误:

    错误: 找不到符号message.readFromParcel(_reply);
    

    在子进程修改完数据之后,如果要把数据重新返回给主进程,需要把数据通过Message的 readFromParcel() 方法重新赋值给主进程的 message 对象。所以需要在 Message 类中添加 readFromParcel() 方法,如下:

    fun readFromParcel(parcel: Parcel) {
        content = parcel.readString()
        isSendSuccess = parcel.readByte().toInt() == 1
    }
    

    接着在运行一下代码就会发现主进程中的数据已经被修改了

  • out :和 in 正好相反,数据不能发送到子进程,但是子进程的数据可以发送到主进程

总结

​ 上面 通过 AIDL 实现了跨进程的通信。

​ 使用 AIDL 时,有些需要注意的地方:只支持 基本数据类型 和 实现了 Parcelable 接口的对象。

​ 实现的步骤:

​ 1,创建 AIDL 接口文件

​ 2,创建一个服务端,onBind 方法中返回一个 IBind 对象,这个对象是通过 AIDL 接口.Stub 得到的,

​ 3,创建一个客户端,然后使用 bindService 的方式启动 服务,服务端的 onBind 执行完后,客户端就可以获取到服务端传过来的值(即把 Binder 对象转为 AIDL接口),

4,使用传过来的 AIDL 接口就可以调用到服务端了。


GitHub :IPCDemo

参考自艺术探索
参考自慕课网视频

原创文章 119 获赞 77 访问量 3万+

猜你喜欢

转载自blog.csdn.net/baidu_40389775/article/details/105409365