一 多进程之间的通信

由于不同进程所拥有的地址是两块不同的地址空间,所以不能直接通过共享内存共享数据了。

Linux常用跨进程通信方式:管道,信号量,共享内存,socket

Android常用跨进程通信方式:Intent ,共享文件,SharedPreferences,Binder,socket,基于Binder的Messenger.下面详细学习进程间的通信方式。

二,进程之间的通信方式

1.Binder

Binder是安卓中最重要的IPC通信方式了,一个应用进程的创建,应用进程中组件的生命周期的调度,系统服务的使用...处处都是binder,从而也可以看出,安卓中进程间的通信是非常频繁的,一句话来说,在Binder通信机制的强有力支持下,安卓进程间进行了友好的数据交互。


Binder是进程间通信的一种架构,这个架构分为:

  1. 服务端接口(Bn端)
  2. 客户端接口(Bp端)
  3. Binder驱动

1.1  拆分xxx.aidl文件生成类
     由aidl生成的java文件(一个接口文件;该接口文件中嵌套一个抽象的类Stub,该类实现外边的接口文件,没有实现具体接口方法,接口方法由其子类实现;抽象类中有一个proxy类,该类是抽象Stub的代理类,同时也实现了最外层的接口文件,proxy文件的作用是将客户端的输入包装成统一形式,具体的业务实现在服务端 Stub的子类中)由aidl文件系统帮我们生成的三个文件,如下:

a) 接口 IXXX extends IInterface

b) abstract class Stub extends Binder imp IXXX    该类重写了binder的 OnTransact方法:

public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags){.................case TRANSACTION_startScan:
{
data.enforceInterface(DESCRIPTOR);  //校验
java.lang.String[] _arg0;
_arg0 = data.createStringArray(); //读取参数
com.miui.guardprovider.aidl.IVirusObserver _arg1;
_arg1 = com.miui.guardprovider.aidl.IVirusObserver.Stub.asInterface(data.readStrongBinder());//读取参数boolean _arg2;
_arg2 = (0!=data.readInt());//读取参数
int _result = this.startScan(_arg0, _arg1, _arg2);//调用服务端具体实现函数
reply.writeNoException();
reply.writeInt(_result);//返回给客户端的结果
return true;
}
............................
}

c) static class Proxy imp IXXX

服务端的代理类,跨进程通信中,客户端使用代理类proxy调用服务

int startScan(java.lang.String[] paths, com.miui.guardprovider.aidl.IVirusObserver virusObserver, boolean isCloud) throws android.os.RemoteException{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);//对应服务端data.enforceInterface(DESCRIPTOR)
_data.writeStringArray(paths);//写入参数
_data.writeStrongBinder((((virusObserver!=null))?(virusObserver.asBinder()):(null)));写入参数
_data.writeInt(((isCloud)?(1):(0)));写入参数
mRemote.transact(Stub.TRANSACTION_startScan, _data, _reply, 0);//调用服务端的方法
_reply.readException();
_result = _reply.readInt();
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}

总结一个完整的通信过程:
    客户端进程得到Bp端引用,调用目标方法,目标方法中通过mRemote调用 transact()将目标函数的参数写入包裹,再通过底层binder驱动转发到服务端,经过OnTransact分发调用具体实现,最终调用到service中的具体业务实现。

 1.2 客户端如何获得服务端的Binder对象引用

a)系统服务:使用getSystemService()方法获取的服务,系统服务一般不使用Service类实现,一般都是继承binder类。系统服务由ServiceManager来管理,系统服务在使用前向ServiceManager进行注册,客户端使用服务时向ServiceManager获取服务的引用。注意ServiceManager也是一个系统服务,它的代理架构跟其他系统服务一样的

b)应用程序自定义服务:客户端服务则必须基于Service类来编写,通过bindService获取相关服务的binder引用,asInterface提供了统一的查询接口,如果IPC通信,返回Proxy对象引用,如果进程内部使用服务则返回 Stub的具体实现类对象引用。

总结:两种获取方式最终都是通过AMS服务查询得到服务端的Binder引用

1.3 binder框架扮演的角色

binder框架在整个通信过程中不做具体的业务实现,整个框架只负责运输数据,将客户端的请求精确传达给服务端,同时将服务端的处理结果传递给客户端。只做数据传输不做具体业务,从而满足快进程通信。

1.4 Framework层的binder架构与native层的binder架构之间的关系

framework层的binder通过JNI调用native的binder架构,所以framework层(Java层)对native层(c/c++层)进行了一层包装,提供给应用层进行调用。native层binder是C/S架构,framework层的架构与相关类的设计原理与native层类似。所以理解了任意一层都很好理解另一层.

2.ContentProvider

2.1原理
ContentProvider底层实现是binder
ContentProvider的唯一标识android:authorities="xxx"

2.2数据组织
contentprovider是提供数据访问的接口,至于底层数据的组织可以由Sqlite数据库存储组织(持久化),内存List组织,MatrixCursor组织(轻量级)

2.3数据访问
contentprovider通过Uri来区分外界要访问的数据集合
一般为了区分要访问的数据:定义唯一的Uri 和Uri_code,再通过UriMatcher将两者关联起来。
数据访问流程:根据Uri--->Uri_code--->表名--->访问数据

2.4权限校验
ContentProvider是对外界提供数据的,为了对数据进行保护,所以需要相关权限验证
第三方应用进程访问时需要声明访问权限。(由此想到,不管是service,ContentProvider,还是broadcast,对访问者来说他们都是一种受保护的资源,所以整个访问过程中需要添加权限校验。不能随便访问。要安全的获取数据和服务,android是一个开源的系统,在开源的系统上防范变得很重要)

2.5线程安全
需要注意的是CRUP四大方法是存在多线程并发访问,因此方法内部要做好线程同步。

参考:https://blog.csdn.net/zhanglianyu00/article/details/78390824

3.Socket

在Android API中,有几个类对localsocket进行了封装,不仅可以用来应用程序之间进行IPC通信,还可以跨应用程序层和Linux层运行的程序进行通信

https://blog.csdn.net/qq_695538007/article/details/41513845

通过Socket套接字进行跨进程的通信,eg:让服务端循环监听一个端口,客户端请求连接服务端。Socket本身支持传输任意字节流

android中的socket通信,netd例子:https://www.jianshu.com/p/f752b2019c97

目前做过一个手机定制,接触了android中的socket通信。总结一下:

platform/system/netd/server/DnsProxyListener.cpp//DNS解析,运行在netd进程
frameworks/base / services/java/com/android/server/XXXNetworkManagementService.java//framework层,负责跟native层的netd通信,运行在system_server进程
frameworks/base / native/services/netd/CommandListenerXXX.cpp//native层的netd

总结:system_server进程跟netd进程通过socket通信。system_server端发送命令给netd进程,netd进程收到命令后解析执行命令。
域名黑白名单的逻辑是:上层将黑白名单传递给netd进程,netd进程中维护黑白名单列表。每次域名解析时根据黑白名单控制域名联网

private void listenToSocket() throws IOException {LocalSocket socket = null;try {socket = new LocalSocket();LocalSocketAddress address = determineSocketAddress();socket.connect(address);//连接netd进程InputStream inputStream = socket.getInputStream();synchronized (mDaemonLock) {mOutputStream = socket.getOutputStream();}mCallbacks.onDaemonConnected();FileDescriptor[] fdList = null;byte[] buffer = new byte[BUFFER_SIZE];int start = 0;while (true) {int count = inputStream.read(buffer, start, BUFFER_SIZE - start);if (count < 0) {loge("got " + count + " reading with start = " + start);break;}fdList = socket.getAncillaryFileDescriptors();// Add our starting point to the count and reset the start.count += start;start = 0;for (int i = 0; i < count; i++) {if (buffer[i] == 0) {// Note - do not log this raw message since it may contain// sensitive datafinal String rawEvent = new String(buffer, start, i - start, StandardCharsets.UTF_8);boolean releaseWl = false;try {final NativeDaemonEvent event =NativeDaemonEvent.parseRawEvent(rawEvent, fdList);log("RCV <- {" + event + "}");if (event.isClassUnsolicited()) {// TODO: migrate to sending NativeDaemonEvent instancesif (mCallbacks.onCheckHoldWakeLock(event.getCode())&& mWakeLock != null) {mWakeLock.acquire();releaseWl = true;}Message msg = mCallbackHandler.obtainMessage(event.getCode(), uptimeMillisInt(), 0, event.getRawEvent());if (mCallbackHandler.sendMessage(msg)) {releaseWl = false;}} else {mResponseQueue.add(event.getCmdNumber(), event);}} catch (IllegalArgumentException e) {log("Problem parsing message " + e);} finally {if (releaseWl) {mWakeLock.release();}}start = i + 1;}}if (start == 0) {log("RCV incomplete");}// We should end at the amount we read. If not, compact then// buffer and read again.if (start != count) {final int remaining = BUFFER_SIZE - start;System.arraycopy(buffer, start, buffer, 0, remaining);start = remaining;} else {start = 0;}}} catch (IOException ex) {loge("Communications error: " + ex);throw ex;} finally {synchronized (mDaemonLock) {if (mOutputStream != null) {try {loge("closing stream for " + mSocket);mOutputStream.close();} catch (IOException e) {loge("Failed closing output stream: " + e);}mOutputStream = null;}}try {if (socket != null) {socket.close();}} catch (IOException ex) {loge("Failed closing socket: " + ex);}}}

4.文件

三,为什么 Android 要采用 Binder 作为 IPC 机制?
 参考大神gityuan知乎上的回答:https://www.zhihu.com/question/39440766/answer/89210950

作者:Gityuan
链接:https://www.zhihu.com/question/39440766/answer/89210950
来源:知乎

在开始回答 前,先简单概括性地说说Linux现有的所有进程间IPC方式:

1. 管道:在创建时分配一个page大小的内存,缓存区大小比较有限;
2. 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信;
3. 共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;
4. 套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信;
5. 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
6. 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等;

Android的内核也是基于Linux内核,为何不直接采用Linux现有的进程IPC方案呢,难道Linux社区那么多优秀人员都没有考虑到有Binder这样一个更优秀的方案,是google太过于牛B吗?事实是真相并非如此,请细细往下看,您就明白了。

-------------------------------------------------------------------------------------------------------------------------------------------

接下来正面回答这个问题,从5个角度来展开对Binder的分析:

(1)从性能的角度 数据拷贝次数:Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存。

(2)从稳定性的角度
Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。

仅仅从以上两点,各有优劣,还不足以支撑google去采用binder的IPC机制,那么更重要的原因是:

(3)从安全的角度
传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐射数据、后台造成手机耗电等等问题,传统Linux IPC无任何保护措施,完全由上层协议来确保。

Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行。Android 6.0,也称为Android M,在6.0之前的系统是在App第一次安装时,会将整个App所涉及的所有权限一次询问,只要留意看会发现很多App根本用不上通信录和短信,但在这一次性权限权限时会包含进去,让用户拒绝不得,因为拒绝后App无法正常使用,而一旦授权后,应用便可以胡作非为。

针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。

Android中权限控制策略有SELinux等多方面手段,下面列举从Binder的一个角度的权限控制:
Android源码的Binder权限是如何控制? -Gityuan的回答

传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。从安全角度,Binder的安全性更高。

说到这,可能有人要反驳,Android就算用了Binder架构,而现如今Android手机的各种流氓软件,不就是干着这种偷窥隐射,后台偷偷跑流量的事吗?没错,确实存在,但这不能说Binder的安全性不好,因为Android系统仍然是掌握主控权,可以控制这类App的流氓行为,只是对于该采用何种策略来控制,在这方面android的确存在很多有待进步的空间,这也是google以及各大手机厂商一直努力改善的地方之一。在Android 6.0,google对于app的权限问题作为较多的努力,大大收紧的应用权限;另外,在Google举办的Android Bootcamp 2016大会中,google也表示在Android 7.0 (也叫Android N)的权限隐私方面会进一步加强加固,比如SELinux,Memory safe language(还在research中)等等,在今年的5月18日至5月20日,google将推出Android N。

话题扯远了,继续说Binder。

(4)从语言层面的角度
大家多知道Linux是基于C语言(面向过程的语言),而Android是基于Java语言(面向对象的语句),而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面,Binder更适合基于面向对象语言的Android系统,对于Linux系统可能会有点“水土不服”。

另外,Binder是为Android这类系统而生,而并非Linux社区没有想到Binder IPC机制的存在,对于Linux社区的广大开发人员,我还是表示深深佩服,让世界有了如此精湛而美妙的开源系统。也并非Linux现有的IPC机制不够好,相反地,经过这么多优秀工程师的不断打磨,依然非常优秀,每种Linux的IPC机制都有存在的价值,同时在Android系统中也依然采用了大量Linux现有的IPC机制,根据每类IPC的原理特性,因时制宜,不同场景特性往往会采用其下最适宜的。比如在Android OS中的Zygote进程的IPC采用的是Socket(套接字)机制,Android中的Kill Process采用的signal(信号)机制等等。而Binder更多则用在system_server进程与上层App层的IPC交互

(5) 从公司战略的角度

总所周知,Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受GPL保护的Linux Kernel是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层Kernel,那么也必须遵循GPL协议。

而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下 开源与商业化共存的一个成功典范。

有了这些铺垫,我们再说说Binder的今世前缘

Binder是基于开源的 OpenBinder实现的,OpenBinder是一个开源的系统IPC机制,最初是由 Be Inc. 开发,接着由Palm, Inc.公司负责开发,现在OpenBinder的作者在Google工作,既然作者在Google公司,在用户空间采用Binder 作为核心的IPC机制,再用Apache-2.0协议保护,自然而然是没什么问题,减少法律风险,以及对开发成本也大有裨益的,那么从公司战略角度,Binder也是不错的选择。

另外,再说一点关于OpenBinder,在2015年OpenBinder以及合入到Linux Kernel主线 3.19版本,这也算是Google对Linux的一点回馈吧。

综合上述5点,可知Binder是Android系统上层进程间通信的不二选择。

------------------------------------------------------------------------------------------------------------------------------------------
接着,回答楼主提到的D-Bus

也采用C/S架构的IPC机制,D-Bus是在用户空间实现的方法,效率低,消息拷贝次数和上下文切换次数都明显多过于Binder。针对D-Bus这些缺陷,于是就产生了kdbus,这是D-Bus在内核实现版,效率得到提升,与Binder一样在内核作为字符设计,通过open()打开设备,mmap()映射内存。

(1)kdbus在进程间通信过程,Client端将消息在内存的消息队列,可以存储大量的消息,Server端不断从消息队里中取消息,大小只受限内存;
(2)Binder的机制是每次通信,会通信的进程或线程中的todo队里中增加binder事务,并且每个进程所允许Binder线程数,google提供的默认最大线程数为16个,受限于CPU,由于线程数太多,增加系统负载,并且每个进程默认分配的(1M-8K)大小的内存。

而kdbus对于内存消耗较大,同时也适合传输大量数据和大量消息的系统。Binder对CPU和内存的需求比较低,效率比较高,从而进一步说明Binder适合于移动系统Android,但是,也有一定缺点,就是不同利用Binder输出大数据,比如利用Binder传输几M大小的图片,便会出现异常,虽然有厂商会增加Binder内存,但是也不可能比系统默认内存大很多,否则整个系统的可用内存大幅度降低。

最后,简单讲讲Android Binder架构

Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_server进程后,在system_server进程中出初始化支持整个Android framework的各种各样的Service,而这些Service从大的方向来划分,分为Java层Framework和Native Framework层(C++)的Service,几乎都是基于BInder IPC机制。

  1. Java framework:作为Server端继承(或间接继承)于Binder类,Client端继承(或间接继承)于BinderProxy类。例如 ActivityManagerService(用于控制Activity、Service、进程等) 这个服务作为Server端,间接继承Binder类,而相应的ActivityManager作为Client端,间接继承于BinderProxy类。 当然还有PackageManagerService、WindowManagerService等等很多系统服务都是采用C/S架构;
  2. Native Framework层:这是C++层,作为Server端继承(或间接继承)于BBinder类,Client端继承(或间接继承)于BpBinder。例如MediaPlayService(用于多媒体相关)作为Server端,继承于BBinder类,而相应的MediaPlay作为Client端,间接继承于BpBinder类。

四,Binder为什么是一次内存拷贝?
参考:https://www.cnblogs.com/qingchen1984/p/5212755.html
binder实现了mmap

Android跨进程通信相关推荐

  1. Android 跨进程通信大总结

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/111553746 本文出自[赵彦军的博客] 文章目录 1.Android进程 2.修 ...

  2. 【朝花夕拾】Android跨进程通信总结篇

    前言 原文:https://www.cnblogs.com/andy-songwei/p/10256379.html 只要是面试高级工程师岗位,Android跨进程通信就是最受面试官青睐的知识点之一. ...

  3. 【朝花夕拾】Android性能篇之(七)Android跨进程通信篇

    前言 转载请声明,转自[https://www.cnblogs.com/andy-songwei/p/10256379.html],谢谢! 只要是面试高级工程师岗位,Android跨进程通信就是最受面 ...

  4. 【朝花夕拾】Android性能篇之(七)Android跨进程通信篇...

    前言 原文:https://www.cnblogs.com/andy-songwei/p/10256379.html 只要是面试高级工程师岗位,Android跨进程通信就是最受面试官青睐的知识点之一. ...

  5. Android跨进程通信Binder机制与AIDL实例

    文章目录 进程通信 1.1 进程空间划分 1.2 跨进程通信IPC 1.3 Linux跨进程通信 1.4 Android进程通信 Binder跨进程通信 2.1 Binder简介 2.2 Binder ...

  6. Android 跨进程通信(一)

    Android 跨进程通信 Android 本身提供一四种方式进行实现跨进程通信,他们也分别是Android的四大组件.分别是:Activity,Content Provider,Broadcast和 ...

  7. Android跨进程通信一 Messenger

    实现客户端与服务端之间的交互 说明:         Messenger是信使的意思,从它的名字就可以了解到它充当着信差的角色.Android通过它实现跨进程通信,主要有客户端信使与服务端信使两种角色 ...

  8. Android - 跨进程通信(IPC) 另一种便捷实现 详解

    文章目录 1. 写在前面 2. 跨进程通信的实现 3. 扩展思考 4. 参考资料 1. 写在前面 看到此图有何感想,这是另一种便捷的实现方式,我们先来看看其它的几种方式. Android 进程间通信 ...

  9. 【Binder】Android 跨进程通信原理解析

    前言 在Android开发的过程中,用到跨进程通信的地方非常非常多,我们所使用的Activity.Service等组件都需要和AMS进行跨进程通信,而这种跨进程的通信都是由Binder完成的. 甚至一 ...

最新文章

  1. 隐马尔科夫模型HMM(一)HMM模型
  2. 计算机本科学位有用吗_我应该回到学校获得计算机科学学位吗?
  3. IDEA常用快捷键!!
  4. 手机蓝牙如何减少延时_如何使用车载蓝牙播放手机音乐的方法
  5. boost::describe模块实现console的测试程序
  6. 【嵌入式】C语言高级编程-语句表达式(03)
  7. java 十进制转十六进制、十进制转二进制、二进制转十进制、二进制转十六进制...
  8. 同济大学 线性代数 第六版 pdf_线性代数(第六版)【课后习题答案】
  9. 基于STM32MP157调试MIPI-DSI屏幕
  10. 图像与视频处理中的优化方法
  11. 网络编程中,同步传输和异步传输有什么区别
  12. 唐巧的《iOS开发进阶》 - 读后感
  13. 高阶函数,太有用啦!
  14. 20个非常有用的Python单行代码
  15. 几个Android云测试
  16. XDOJ 233/237-字符串复制
  17. adb连接夜神模拟器提示:adb unable to connect to 127.0.0.162001 cannot connect to 127.0.0.16200 由于目标 计算机积极拒绝
  18. 英国开发者年龄歧视为29岁,女程序员幸福指数略高于男性 | 全球开发者幸福指数报告
  19. c语言注释两种,C语言有几种注释方式
  20. 来个大佬帮看下这个密文如何解密

热门文章

  1. python sobel滤波_sobel滤波器在imag中的应用
  2. Spring Security、oauth2、单点登陆SSO的关系
  3. python箱线图异常值_Python数据清洗--异常值识别与处理01
  4. R进行构建回归时出现的变数的长度不唯一的报错
  5. C++ 格式化字符串方式总结
  6. notify和notify的区别
  7. 前端开发——JavaScript的数据类型和引用类型
  8. 上行参考信号DMRS for PUSCH
  9. Win7下Android开发环境的搭建(更新于2015/3)
  10. (附源码)springboot宠物领养系统 毕业设计 241104