06. Android Binder图解 小米权威

作者: 鹏城十八少 | 来源:发表于2022-01-27 10:49 被阅读0次

    很多BAT也不一定能懂的binder机制!
    因为搞懂binder需要会c,linux内核知识。看java根本就看不懂!
    我同事从小米跳槽过来,干安卓framework层10年,是小米的专家级别
    然后他把binder驱动层全部和我讲解了一遍,然后我这边做个笔记分享给大家。

    1642733316778-03b.png

    分6篇文字讲解:

    1. Android Binder图解 小米系统专家 解析Service 的addService注册过程 (安卓12)
    2. Android Binder图解 小米系统专家 解析 ServiceManager和binder通信 (安卓12)
    3. Android Binder图解 小米系统专家 解析binder驱动层解析binder通信过程 (安卓12)
    4. Android Binder图解 小米系统专家 从binder java层解析binder整个流程 (安卓12)
    5. Android Binder图解 小米系统专家 解析binder总结调用流程 (安卓12)
    6. Android Binder图解 小米系统专家 解析binder面试一网打尽(安卓12)
    1642733316778-03b.png

    问题1: 描述binder通信原理

    问题2:Binder Driver 如何在内核空间中做到一次拷贝的?

    问题3:简单讲讲 binder 驱动吧

    从 Java 层来看就像访问本地接口一样,客户端基于 BinderProxy 服务端基于 IBinder 对象,从 native 层来看来看客户端基于 BpBinder 到 ICPThreadState 到 binder 驱动,服务端由 binder 驱动唤醒 IPCThreadSate 到 BbBinder 。跨进程通信的原理最终是要基于内核的,所以最会会涉及到 binder_open 、binder_mmap 和 binder_ioctl这三种系统调用。

    Binder 的完整定义

    • 从进程间通信的角度看,Binder 是一种进程间通信的机制;
    • 从 Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;
    • 从 Client 进程的角度看,Binder 指的是 Binder 代理对象,是 Binder 实体对象的一个远程代理;
    • 从传输过程的角度看,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象对一点点特殊处理,自动完成代理对象和本地对象之间的转换。

    binder驱动有什么用? 匿名内存有什么用?

    哪里过程经过了binder驱动?有看过Binder.cc文件吗?

    binder,为什么c不能访问a和b的进程中的信息 ?

    binder机制的本质:内存共享,不是发送数据,所以要深入研究内存

    binder驱动和内核的关系: 在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。

    Binder 驱动加载过程中有哪些重要的步骤

    答:cs架构,client:

    biner驱动:

    server:

    他们在不同进程。server会把自己的能力定义成接口,server注册到server里面去,然后client通过server代理调用server的方法。中间都会经过binder

    问题4:Binder框架中ServiceManager的作用?

    Binder框架 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder驱动,其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。如下图所示:

    [图片上传失败...(image-9fa5d4-1643251732134)]

    • Server&Client:服务器&客户端。在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。
    • ServiceManager(如同DNS域名服务器)服务的管理者,将Binder名字转换为Client中对该Binder的引用,使得Client可以通过Binder名字获得Server中Binder实体的引用。
    • Binder驱动(如同路由器):负责进程之间binder通信的建立,计数管理以及数据的传递交互等底层支持。

    ServiceManager的作用是怎么样的?

    ServiceManager最核心的两个功能为查询和注册服务:

    • 注册服务:记录服务名和handle信息,保存到svclist列表;
    • 查询服务:根据服务名查询相应的的handle信息。

    首先Service会通过addService将binder实体注册到ServiceManager中去,Client如果想要使用Servcie,就需要通过getService向ServiceManager请求该服务

    问题5:binder驱动是如何和servermanager通信的?

    问题6:binder相关,如何实现binder异步调用(头条)

    Binder请求的同步与异步

    如果我在子线程跨进程调用会怎么样?如何实现binder异步调用?

    很多人都会说,Binder是对Client端同步,而对Service端异步,其实并不完全正确,在单次Binder数据传递的过程中,其实都是同步的。只不过,Client在请求Server端服务的过程中,是需要返回结果的,即使是你看不到返回数据,其实还是会有个成功与失败的处理结果返回给Client,这就是所说的Client端是同步的。

    问题7:****如何实现****binder异步调用?

    客户端进行远程 RPC 请求时,线程会挂起,等待结果,由此也可知,AIDL 的调用过程是同步的

    IDL 的调用过程是同步的,当我们需要服务端做耗时操作时,肯定是不能使用同步调用的,否则轻者影响用户体验,重者直接 ANR 或者应用崩溃。那么如何使 AIDL 的调用过程是异步的呢?

    其实也很简单,只需要把调用放到非 UI 线程即可,如果要对调用的返回做 UI 更新的话,再通过 Handler 处理即可

    看完图总结:

    也就是客户端通过 Proxy 访问 Binder 驱动,然后 Binder 驱动调用 Stub,而 Stub 中调用我们的业务逻辑。这里的 ProxyStub 用来统一接口函数,

    Proxy 用来告诉我们远程服务中有哪些可用的方法,持有serverr端的代理对象binder

    而具体的业务逻辑则由 Stub 来实现。

    Binder 的进程通信就发生在 ProxyStub 之间。

    而且所谓的服务端和客户端都是相对而言的,服务端不仅可以接收和处理消息,而且可以定时往客户端发送数据,与此同时服务端使用Proxy类跨进程调用,相当于充当了”Client”。

    这样,客户端只需要和 Proxy 打交道,服务端只需要和 Stub 打交道,调理清晰很多。图如下:

    [图片上传失败...(image-af291e-1643251732134)]

    问题8:****intent传值,大小限制和什么有关?binder?binder如何限制这个大小?跨进程传递大图,你能想到哪些方案呢?

    intent传值,大小限制和什么有关?binder?binder如何限制这个大小?

    跨进程传递大图,你能想到哪些方案呢?

    需求背景

    项目中有个需求是这样的,在主进程Activity 中选择或者编辑一张背景图产生一个bitmap 对象,要传递给 B进程(推流进程)作为推流引擎的背景图,这个bitmap 有可能比较大,因为要尽量保证清晰度,所以这个bitmap还有可能比较大,所以必然会涉及到跨进程传输大型bitmap 的问题。

    第一种方式:传递路径,通过文件写入。contentProvider封装,然后可以

    第二种方式,如果是startActivity。通过putbinder

    之前小数据:开辟一个ashmem保存,这个数据有大小限制的

    用putbinder话:会parcel的缓冲区里分配一块空间来保存这个数据

    第三种方式,AIDL:有什么办法解决吗?

    源码分析:

    https://blog.csdn.net/ylyg050518/article/details/97671874

    1M-8k=1024-8k=1M-pageSIze4k。这个值在c代码里面。多少个页*

    https://blog.csdn.net/u011033906/article/details/89316543

    (frameworks/native/libs/binder/****ProcessState****.cpp)

    ProcessState.cpp

    define DEFAULT_BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)#define DEFAULT_MAX_BINDER_THREADS 0

    如果是异步的话,大小又是多少呢?

    是上面的大小除以2。可以看源码

    https://blog.csdn.net/ylyg050518/article/details/97671874

    问题9:aidl实现原理,如何实现回调?

    onconnect之后会得到ibinder对象,这个过程是怎么样的?

    <pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-size: 0.8rem;">private ServiceConnection mStepConnect = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
    mServiceAutoStepInterface = IServiceAutoStepInterface.Stub.asInterface(service);
    try {
    mServiceAutoStepInterface.registerListener(mStepCountListener);
    } catch (Throwable e) {
    }
    }
    </pre>

    源码:从binderserver------AMS----ActivityThrad.可以看到

    问题10:****讲讲AIDL?如何优化多模块都使用AIDL的情况?

    AIDL(Android Interface Definition Language,Android接口定义语言):如果在一个进程中要调用另一个进程中对象的方法,可使用AIDL生成可序列化的参数,AIDL会生成一个服务端对象的代理类,通过它客户端可以实现间接调用服务端对象的方法。

    AIDL的本质是系统提供了一套可快速实现Binder的工具。关键类和方法:

    • AIDL接口:继承IInterface。
    • Stub类:Binder的实现类,服务端通过这个类来提供服务。
    • Proxy类:服务端的本地代理,客户端通过这个类调用服务端的方法。
    • asInterface():客户端调用,将服务端返回的Binder对象,转换成客户端所需要的AIDL接口类型的对象。如果客户端和服务端位于同一进程,则直接返回Stub对象本身,否则返回系统封装后的Stub.proxy对象。
    • asBinder():根据当前调用情况返回代理Proxy的Binder对象。
    • onTransact():运行在服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。
    • transact():运行在客户端,当客户端发起远程请求的同时将当前线程挂起。之后调用服务端的onTransact()直到远程请求返回,当前线程才继续执行。

    当有多个业务模块都需要AIDL来进行IPC,此时需要为每个模块创建特定的aidl文件,那么相应的Service就会很多。必然会出现系统资源耗费严重、应用过度重量级的问题。解决办法是建立Binder连接池,即将每个业务模块的Binder请求统一转发到一个远程Service中去执行,从而避免重复创建Service。

    工作原理:每个业务模块创建自己的AIDL接口并实现此接口,然后向服务端提供自己的唯一标识和其对应的Binder对象。服务端只需要一个Service并提供一个queryBinder接口,它会根据业务模块的特征来返回相应的Binder对象,不同的业务模块拿到所需的Binder对象后就可以进行远程方法的调用了。

    问题11:binder 复习stub proxy代表的含义?如何得到服务端的代理和客户端的代理

    答:它们是生成的java里面的2个类

    stub :Stub是用于在服务端进程创建的对象 。得到代理,调用remote的onTranslAct方法

    proxy:是用于在客户端进程创建的代理对象 。代理,先序列化。在里面调用translact方法

    是服务端的Stub对象在客户端进程的代理对象,客户端进程通过Proxy对象发出方法调用然后通过Binder驱动后最终调用服务端进程的Stub对象的方法。每一个Stub远程对象都在客户端进程中对应有一个本地代理对象Proxy。

    问题12:.binder进程间通信可以调用原进程方法吗?

    答:可以,通过代理的形式调用

    [图片上传失败...(image-9f1d00-1643251732122)]

    在使用 Binder 时基本都是调用 framework 层封装好的方法,AIDL 就是 framework 层提供的傻瓜式是使用方式。假设服务已经注册完,客户端怎么执行服务端的方法:

    首先通过 ServiceManager 获取到服务端的 BinderProxy 代理对象,通过调用 BinderProxy 将参数,方法标识(例如:TRANSACTION_test,AIDL中自动生成)传给 ServiceManager,同时客户端线程进入等待状态。

    ServiceManager 将用户空间的参数等请求数据复制到内核空间,并向服务端插入一条执行执行方法的事务。事务执行完通知 ServiceManager 将执行结果从内核空间复制到用户空间,并唤醒等待的线程,响应结果,通讯结束

    问题13:AIDL的in和out的了解么

    AIDL平时开发过程中用的还真是少,网上找的一个解释:

    • in、out、inout表示跨进程通信中数据的流向(基本数据类型默认是in,非基本数据类型可以使用其它数据流向out、inout)。
    • in 表示数据只能由客户端流向服务端。(表现为服务端修改此参数,不会影响客户端的对象)
    • out 表示数据只能由服务端流向客户端。(表现为服务端收到的参数是空对象,并且服务端修改对象后客户端会同步变动)
    • inout 则表示数据可在服务端与客户端之间双向流通。(表现为服务端能接收到客户端传来的完整对象,并且服务端修改对象后客户端会同步变动)
      详细解释可以看看:AIDL参数中in、out、inout的区别

    AIDL中的定向 tag 表示了在跨进程通信中数据的流向

    • in、out、inout表示跨进程通信中数据的流向(基本数据类型默认是in,非基本数据类型可以使用其它数据流向out、inout)
    • in 表示数据只能由客户端流向服务端。(表现为服务端修改此参数,不会影响客户端的对象)
    • out 表示数据只能由服务端流向客户端。(表现为服务端收到的参数是空对象,并且服务端修改对象后客户端会同步变动)
    • inout 则表示数据可在服务端与客户端之间双向流通。(表现为服务端能接收到客户端传来的完整对象,并且服务端修改对象后客户端会同步变动)
      详细解释可以看看:AIDL参数中in、out、inout的区别
    • [图片上传失败...(image-306fe8-1643251732134)]

    问题14:系统服务与bindService等启动的服务的区别

    使用系统服务一般都是通过ServiceManager的getService得到服务的句柄,这个过程其实就是去ServiceManager中查询注册系统服务。而bindService启动的服务,主要是去ActivityManagerService中去查找相应的Service组件,最终会将Service内部Binder的句柄传给Client。

    问题15:binder怎么验证pid?

    pid与uid的区别

    答:pid是进程ID,PID是进程的身份标志,系统给每个应用分配独一无二的PID(一个应用可能有多个进程,每个进程有唯一的PID)

    进程终止后PID会被系统回收,再次打开应用会重新分配一个PID。

    UID在linux中是用户的ID,用于权限的管理。在android中,由于android是单用户系统,所以uid被用于实现数据共享。

    通过Activity的启动流程会验证PID的!

    https://blog.csdn.net/windskier/article/details/6921672

    问题16:****权限验证

    就算是公交车,上车也得嘀卡对不,如果希望我们的服务进程不想像公交车一样谁想上就上,那么我们可以加入权限验证。

    介绍两种常用验证方法:

    1. 在服务端的onBind中校验自定义permission,如果通过了我们的校验,正常返回Binder对象,校验不通过返回null,返回null的情况下客户端无法绑定到我们的服务;

      代码如下:

    [图片上传失败...(image-f041fd-1643251732133)]

    1. 在服务端的onTransact方法校验客户端包名****,不通过校验直接return false,校验通过执行正常的流程。

    [图片上传失败...(image-d4ab05-1643251732134)]

    <pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-family: "JetBrains Mono", monospace; font-size: 0.817rem;">private Binder mBinder = new IImageManager.Stub() {
    @Override
    public void doTask(int count) throws RemoteException {
    Log.d("ImageManagerService", "doTask");
    Thread thread = new Thread() {
    @Override
    public void run() {
    Log.e(TAG, "startImageScanner:size-->");
    }
    };
    thread.start();
    }

    @Override
    

    public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
    return super.onTransact(code, data, reply, flags);
    }

    @Override
    

    public IBinder asBinder() {
    return super.asBinder();
    }
    };</pre>

    跨进程文件写入如何保证安全

    和sp一样,通过contentProvider。用openFile方法!

    https://blog.csdn.net/fan380485838/article/details/80937414

    问题17:.主进程和子进程间的通信,通过哪块内存区域?

    Android匿名共享内存(Ashmem)原理

    Android匿名共享内存(Ashmem)原理 - 简书 (jianshu.com)问题18:**** 客户端如何将函数形参发送给远程进程中的函数,以及如何将远程进程函数计算结果返回客户端

    1. 定义接口Parcelable,这个接口提供2个重要函数,分别是将对象中的属性写入到数组和从数组中的数据还原对象,每个可以发送到远程函数作为形参的对象只需实现Parcelable对象即可

    [图片上传失败...(image-ba85bd-1643251732134)]

    [图片上传失败...(image-e6a77d-1643251732134)]

    从上面的Binder 的使用流程我们可以知道几个事情:

    1).服务端和客户端是两个进程,客户端调用transact(),服务端实现onTransact()

    1. . Binder的调用,在客户端看来是同步的,transact()从调用到获取结果的过程。

    3). Binder的被调用,在服务端看来是异步,支持多个客户端调用,服务端底层是线程池。所以可能需要注意同步的问题。

    4). 如果服务端和客户端在同一个进程,是不会发生Binder通信的,而是在asInterface返回服务端的对象。

    5). onTransact可以做权限验证,拒绝调用。

    6). 客户端在调用add的时候,内部是调用transact,在等待回应的时候将会被挂起,所以若是耗时操作的话需要开启子线程。

    问题19:****怎么理解页框和页?

    🤔️:页框是指一块实际的物理内存,页是指程序的一块内存数据单元。内存数据一定是存储在实际的物理内存上,即页必然对应于一个页框,页数据实际是存储在页框上的。

    页框和页一样大,都是内核对内存的分块单位。一个页框可以映射给多个页,也就是说一块实际的物理存储空间可以映射给多个进程的多个虚拟内存空间,这也是 mmap 机制依赖的基础规则

    linux使用MMU的机器都采用分页机制。虚拟地址空间以为单位进行划分,而相应的物理地址空间也被划分,其使用的单位称为页帧,页帧和页必须保持相同,因为内存与外部存储器之间的传输是以页为单位进行传输的

    例如,MMU可以通过一个映射项将VA的一页0xb70010000xb7001fff映射到PA的一页0x20000x2fff,如果CPU执行单元要访问虚拟地址0xb7001008,则实际访问到的物理地址是0x2008。

    虚拟内存的哪个页面映射到物理内存的哪个页帧是通过页表(Page Table)来描述的,页表保存在物理内存中MMU会查找页表来确定一个VA应该映射到什么PA。

    问题20:.mmap没调用msync时候,落盘时机。.MMKV原理

    问题21:.linux有哪些多进程方案?

    实际的问题:

    1.A 进程想要 B 进程中某个对象(object)是如何实现的呢?

    应该是不行吧。可以,通过接口返回一个类。AIDL,获取代理。我发现不对,拿到的是接口。如何获取另外一个进程的对象!可以通过接口返回一个对象就可以

    客户端持有远程进程的某个对象引用,然后调用引用类中的函数,远程进程的函数就执行了。我在想,凭什么?学过操作系统都知道,不同的进程之间是不共享资源的。也就是说,客户端持有的这个对象跟远程进程中的实际对象完全是两个不同的对象。客户端调用引用的对象跟远程进程半毛钱关系都没有,凭啥远程进程就调用了执行了?

    ————————————————

    我们所持有的Binder引用(即服务端的类引用)并不是实际真实的远程Binder对象,我们的引用在Binder驱动里还要做一次映射。也就是说,设备驱动根据我们的引用对象找到对应的远程进程。客户端要调用远程对象函数时,只需把数据写入到Parcel,在调用所持有的Binder引用的transact()函数,transact函数执行过程中会把参数、标识符(标记远程对象及其函数)等数据放入到Client的共享内存,Binder驱动从Client的共享内存中读取数据,根据这些数据找到对应的远程进程的共享内存,把数据拷贝到远程进程的共享内存中,并通知远程进程执行onTransact()函数,这个函数也是属于Binder类。远程进程Binder对象执行完成后,将得到的写入自己的共享内存中,Binder驱动再将远程进程的共享内存数据拷贝到客户端的共享内存,并唤醒客户端线程。

    ————————————————--------

    比较好的回答:

    我们已经解释清楚 Client、Server 借助 Binder 驱动完成跨进程通信的实现机制了,但是还有个问题会让我们困惑。A 进程想要 B 进程中某个对象(object)是如何实现的呢?毕竟它们分属不同的进程,A 进程 没法直接使用 B 进程中的 object。

    前面我们介绍过跨进程通信的过程都有 Binder 驱动的参与,因此在数据流经 Binder 驱动的时候驱动会对数据做一层转换。当 A 进程想要获取 B 进程中的 object 时,驱动并不会真的把 object 返回给 A,而是返回了一个跟 object 看起来一模一样的代理对象 objectProxy,这个 objectProxy 具有和 object 一摸一样的方法,但是这些方法并没有 B 进程中 object 对象那些方法的能力,这些方法只需要把把请求参数交给驱动即可。对于 A 进程来说和直接调用 object 中的方法是一样的。

    当 Binder 驱动接收到 A 进程的消息后,发现这是个 objectProxy 就去查询自己维护的表单,一查发现这是 B 进程 object 的代理对象。于是就会去通知 B 进程调用 object 的方法,并要求 B 进程把返回结果发给自己。当驱动拿到 B 进程的返回结果后就会转发给 A 进程,一次通信就完成了。

    [图片上传失败...(image-83ef79-1643251732136)]

    ————————————————

    2.A 进程想要 B 进程中方法怎么实现的呢

    那么请问:在调用2个数相加的方法中,哪里经过了一次拷贝?

    这个过程就是一次拷贝,不仅仅说的是数据的拷贝

    同上,先获取代理,然后调用方法,也就是接口

    3.A 进程想要 B 进程中变量值

    和MMKV一样,进行内存映射

    4.Binder线程、Binder主线程、Client请求线程的概念与区别(网易)

    拿ServerManager进程来说,其主线就是Binder线程

    最后来看一下普通Client的binder请求线程,比如我们APP的主线程,在startActivity请求AMS的时候,APP的主线程成其实就是Binder请求线程,在进行Binder通信的过程中,Client的Binder请求线程会一直阻塞

    binder线程:

    • 一个进程的Binder线程数默认最大是16,超过的请求会被阻塞等待空闲的Binder线程。

      所以,在进程间通信时处理并发问题时,如使用ContentProvider时,它的CRUD(创建、检索、更新和删除)方法只能同时有16个线程同时工作

    好像是15个。和传的大小在同一个源码里面。

    binder线程池的最大线程个数;binder线程池中如果满了,对待新来的任务,会如何处理?此时client端会是什么效果?

    阻塞,等待空闲的binder线程

    4.Server端的binder都是运行在同一个线程里面么?

    不是

    binder里面的进程和线程关系:

    对于底层Binder驱动,通过 binder_procs 链表记录所有创建的 binder_proc 结构体,binder 驱动层的每一个 binder_proc 结构体都与用户空间的一个用于 binder 通信的进程一一对应,且每个进程有且只有一个 ProcessState 对象,这是通过单例模式来保证的。在每个进程中可以有很多个线程,每个线程对应一个 IPCThreadState 对象,IPCThreadState 对象也是单例模式,即一个线程对应一个 IPCThreadState 对象,在 Binder 驱动层也有与之相对应的结构,那就是 Binder_thread 结构体。在 binder_proc 结构体中通过成员变量 rb_root threads,来记录当前进程内所有的 binder_thread。

    Binder 线程池:每个 Server 进程在启动时创建一个 binder 线程池,并向其中注册一个 Binder 线程;之后 Server 进程也可以向 binder 线程池注册新的线程,或者 Binder 驱动在探测到没有空闲 binder 线程时主动向 Server 进程注册新的的 binder 线程。对于一个 Server 进程有一个最大 Binder 线程数限制,默认为16个 binder 线程,例如 Android 的 system_server 进程就存在16个线程。对于所有 Client 端进程的 binder 请求都是交由 Server 端进程的 binder 线程来处理的。

    ————————————————

    线程和进程的区别?

    1、进程是一段正在执行的程序,是资源分配的基本单元,而线程是CPU调度的基本单元。
    2、进程间相互独立进程,进程之间不能共享资源,一个进程至少有一个线程,同一进程的各线程共享整个进程的资源(寄存器、堆栈、上下文)。
    3、线程的创建和切换开销比进程小。

    4. 多进程的使用场景:打电话,加载图片,webview,推送。

    5.服务端进程Crash了,而客户端进程想要调用服务端方法,这样就调用不到了

    此时我们可以给Binder设置一个DeathRecipient对象,当Binder意外挂了的时候,我们可以在DeathRecipient接口的回调方法中收到通知,并作出相应的操作,比如重连服务等等。

    6.一般来说,使用多进程通信会造成如下几方面的问题:

    • 静态成员和单例模式完全失效:独立的虚拟机造成。
    • 线程同步机制完全失效:独立的虚拟机造成。
    • SharedPreferences的可靠性下降:这是因为Sp不支持两个进程并发进行读写,有一定几率导致数据丢失。
    • Application会多次创建:Android系统在创建新的进程时会分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程,自然也会创建新的Application。

    binder面试题:

    https://juejin.cn/post/6870447920911646734

    相关文章

      网友评论

        本文标题:06. Android Binder图解 小米权威

        本文链接:https://www.haomeiwen.com/subject/kmxahrtx.html