您好,登錄后才能下訂單哦!
Android中IPC機制的原理是什么,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
IPC是 Inter-Proscess Communication的縮寫,含義為進程間的通訊或者跨進程通訊,是指兩個進程之間進行數據交換的過程。按操作系統的中的描述,線程是CPU調度最小的單元,同時線程是一種有限的系統資源,而進程是指一個執行單元,在PC和移動設備上指一個程序或者一個應用。一個進程可以包含多個線程,因此進程和線程是包含于被包含的關系。
IPC的使用場景就必須提到多進程,只有面對多進程這種場景下,才需要考慮進程間通訊。多進程的情況分為兩種:***種是一個應用因為某些原因自身需要采用多進程模式來實現,原因有很多,應用特殊原因需要運行的單獨的進程中,或者為了加大一個應用可使用內存所以需要通過多進程來獲取多分內存空間。另外一種情況是:當前應用需要向其他應用獲取數據,由于是兩個應用,所以必須采取跨進程方式來獲取所需要數據。
Android中的多進程模式
開啟Android多進程模式很簡單,就是給四大組件(Activity,Service,Receiver,ContentProvider)在AndroidMenifest中指定android:process屬性。另外還有一種非常規的做法,那就是通過JNI在native層去fork一個新的進程。
給process指定多進程有兩種不同的形式
:remote
進程名以 “:”的含義是指要在進程名前面附加上當前的包名,這個進程屬于當前應用的私有進程,其他應用不可以和他跑在同一個進程。
com.xxx.xxx
這種屬于全局進程,其他應用可以通過ShareUID方式可以和它跑在同一個進程,我們都知道系統會為每個應用分配一個唯一的UID,具有相同UID的應用才能共享數據。兩個應用通過ShareUID跑在同一個進程,是需要相同的ShareUID并且簽名相同才可以。不管它們是不是跑在同一個進程中,具有相同ShareUID的它們可以訪問對方的私有數據,如:data目錄、組件信息等。當然如果是在同一個進程中,除了data目錄、組件信息還能共享內存數據。
多進程運行機制
我們知道Android為每一個應用分配了一個獨立的虛擬機,或者說為每一個進程都分配了一個獨立的虛擬機,不同的虛擬機在內存分配上有不同的地址空間,這就導致在不同的虛擬機訪問同一個類的對象會產生多分副本。
所有運行在不同進程中的四大組件,只要它們之間需要通過內存來共享數據,都會共享失敗,這也是多進程所帶來的主要影響,一般來說,使用多進程會造成如下幾方面的問題。
靜態成員和單例模式完全失效(都是不同的虛擬機)
線程同步機制完全失效(都不是同一塊內存了)
SharePreferences 的可靠性下降(底層通過XML去執行操作的,并發很可能出問題,甚至并發讀、寫都有可能出問題)
Application會多次創建(當一個組件跑在一個新的進程中,由于系統要在創建新的進程同時分配獨立的虛擬機,所以這個過程其實就是啟動一個應用的過程,因此系統又把這個應用重新啟動了一遍,既然都重新啟動了,那么自然會創建新的Application)
IPC基礎概念介紹
Serializable接口
Serializable是Java所提供的一個序列號接口,它是一個空接口,為對象提供標準的序列化和反序列化操作。使用Serializable相當簡單,只需要實現Serializable接口并聲明一個serialVersionUID,其實這個serialVersionUID也不是必需的,如果不聲明這個serialVersionUID也是可以實現序列化的,但是這將會對反序列化過程產生影響。
//序列化 r user = new User("xia","123455"); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt")); out.write(user); out.close(); //反序列化 ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt")); User newUser = (User)in.readObject(); in.close();
serialVersionUID是用來輔助序列化和反序列化過程的,原則上序列化后的數據中serialVersionUID只有和當前類serialVersionUID相同才能夠正常的被反序列化。serialVersionUID的詳細工作機制是這樣的:序列化的時候系統會把當前類的serialVersionUID寫入序列化的文件中(也可能是其他中介),但反序列化的時候會去檢測文件中的serialVersionUID,看它是否和當前類的serialVersionUID一致,如果一致就說明序列化的版本和當前版本是相同的,這個時候可以成功的反序列化,否則就說明當前類和序列化的類相比發生了某些變換,比如成員變量的數量、類型發生了改變,這個時候無法正常反序列化。
一般來說,我們應該手動指定serialVersionUID的值,如1L,也可以根據自身結構自動去生成它的hash值,這樣序列化和反序列化時兩者的serialVersionUID是相同的。如果不指定serialVersionUID的值,反序列化時當前類有所改變,比如增加或者刪除了某些成員變量,那么系統就會重新計算當前類型的hash值并把它賦值給serialVersionUID,這個時候當前類的serialVersionUID就和序列化數據中的serialVersionUID不一致,于是反序列化失敗,程序就會出現crash。所以避免反序列化過程的失敗。比如當版本升級后,我們很可能刪除了某個成員變量也可能增加了一些新的成員變量,這個時候序列化過程仍然能夠成功,程序可以***限度地恢復數據,相反,如果不指定serialVersionUID的話,程序則會掛掉。當然我們還要考慮另外一種情況,如果類的結構發生了非常規性的改變,比如修改了類名,修改了成員變量的類型,這個時候盡管serialVersionUID驗證通過,但是反序列化還是會失敗,因為類結構有了毀滅性的改變,根本無法從老版本的數據中還原出一個新的類結構對象。
靜態成員變量屬于類不屬于對象,所以不會參與序列化過程,其次用transient關鍵字標記的成員變量不參與序列化配置。
- Parceable 接口
Parceable也是一個接口,只有實現這個接口,一個類的對象就可以實現序列化并可以通過Intent和Binder傳遞。
public class User implements Parcelable { public int UserId; public String userName; public boolean isMale; protected User(Parcel in) { //從序列化后的對象中創建原始對象 UserId = in.readInt(); userName = in.readString(); isMale = in.readByte() != 0; } public static final Creator<User> CREATOR = new Creator<User>() { @Override public User createFromParcel(Parcel in) { //從序列化后的對象中創建原始對象 return new User(in); } @Override public User[] newArray(int size) { //創建指定長度的原始對象數組 return new User[size]; } }; @Override public int describeContents() { /** 返回當前對象的內容描述。如果含有文件描述符,返回1,否則返回0,幾乎所有情況都返回0 */ return 0; } @Override public void writeToParcel(Parcel dest, int flags) { /**將當前對象寫入序列化結構中,其中flags,標識有0或1 為1時標識當前對象需要作返回值返回,不能立即釋放資源,幾乎所有情況 都為0**/ dest.writeInt(UserId); dest.writeString(userName); dest.writeByte((byte) (isMale ? 1 : 0)); } }
Parcel內部包裝了可序列化的數據,可以在Binder中自由傳輸,從上述代碼中可以看出,在序列化過程中需要實現的功能有序列化、反序列化和內部描述序列化功能由writeParcel方法完成,最終是通過Parcel中的一系列write方法來完成的。反序列化功能由CREATOR來完成,其內部標明了如何創建序列化對象和數組,并通過Parcel一系列read方法來完成反序列化過程;內容描述功能由describeContents來完成,幾乎所有情況下這個方法都應該返回0,僅當當前對象中存在文件描述符時,此方法返回1.系統已經提供了許多實現Parcelable接口的類,它們都是可以直接序列化的,如:Intent、Bundle、Bitmap等,同時List 和 Map也可以序列化,前提時它們里面每個元素都是可序列化的。
既然Parcelable 和Serializable 都可以用于Intent間的數據傳遞,那么如何選擇了。
- Serializable是Java中的序列化接口,其使用起來簡單但是開銷大,序列化和反序列化過程都需要大量的 I/O操作。
- Parcelable是Android中的序列化方式,更適用于在Android平臺上,它的缺點就是用起來稍微麻煩,但效率很高,這是Android推薦方式,因此,***Parcelable。但通過Parcelable將對象序列化到存儲設備中或將對象序列化后通過網絡傳輸也都是可以的,但是這個過程會稍顯復雜,因此這種情況下建議使用Serializable。
- Binder
Binder是一個非常復雜,這里只是介紹下Binder的使用及上層實現原理。
Binder是Android中的一個類,它實現了IBinder的接口。從IPC角度來說,Binder是Android中一種跨進程的通訊方式,Binder還可以理解為一種虛擬物理設備,它的設備驅動是 /dev/binder,該通訊方式在Linux中沒有;從Android Framework,角度來說,Binder是ServiceManger連接各種Manger(ActivityManger 、WindowManger,等等)和相應的MangeSrervice的橋梁;從Android應用層來說,Binder是客戶端和服務端進行通訊的媒介,當bindSrervice的時候,服務端會返回一個包含了服務端業務調用的Binder對象,通過這個Binder對象,客戶端就可以用獲取服務端提供的服務或者數據,這里的服務包括普通服務和基于AIDL的服務。普通Srervice中的Binder不涉及進程間通信,下面通過AIDL來分析Binder的工作過程。
//Book.java public class Book implements Parcelable{ int id; String type; public Book(int id, String type) { this.id = id; this.type = type; } @Override public String toString() { return "Book{" + "id=" + id + ", type='" + type + '\'' + '}'; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.id); dest.writeString(this.type); } protected Book(Parcel in) { this.id = in.readInt(); this.type = in.readString(); } public static final Creator<Book> CREATOR = new Creator<Book>() { @Override public Book createFromParcel(Parcel source) { return new Book(source); } @Override public Book[] newArray(int size) { return new Book[size]; } }; } ```java // Book.aidl package com.example.xiahao.myapplication; parcelable Book; // IBookManager.aidl package com.example.xiahao.myapplication; // Declare any non-default types here with import statements import com.example.xiahao.myapplication.Book; interface IBookManager { List<Book> getBookList(); void addBook(in Book book); }上面三個文件中,Book.java是一個表示圖書信息的類,它實現了Parcelable接口。Book.aidl 是Book類在AIDL中的聲明。IBookManager.aidl是我們定義的一個接口,里面有兩個方法 getBookList() 和addBook(),其中getBookList用于從遠程服務端獲取圖書列表,而addBook是添加一本書。雖然Book類已經和IBookManager位于相同的包中,但IBookManager仍然需要導入Book類,這就是AIDL的特殊之處。builde的項目,系統為我們在gen目錄下生產IBookManage.java的類,接下來我們需要根據這個系統生成的IBookManag類來分析Binder的工作原理 /* * This file is auto-generated. DO NOT MODIFY. * Original file: /Users/xiahao/Documents/WorkSpace/AndroidStudioProjects/MyApplication/app/src/main/aidl/com/example/xiahao/myapplication/IBookManager.aidl */ }
可以看到根據IBookManager.aidl系統為我們生成了IBookManager.java這個類,它繼承了IInterface這個接口,同時它自己也還是個接口,所以可以在Binder中傳輸的接口都需要繼承IInterface接口。
首先,它聲明了兩個方法getBookList 和 addBook ,這就是我們在IBookManger.aidl中所聲明的方法,同時它還聲明了兩個整數的id分別用于標識這兩個方法,這兩個id用標識在transact過程客戶端請求的到底是哪個方法。接著,還聲明了一個內部類Stub,這個Stub就是一個Biner類,當客戶端和服務端都位于同一個進程中,方法調用不會走跨進程的transact過程,而當兩者位于不同的進程中,方法需要走transact過程,這個邏輯由Stub的內部代理類 Proxy來完成。所以這個接口的實現核心就是它的內部類Stud和Stub的內部代理類 Proxy。
DESCRIPTOR
Binder的唯一標識,一般用于當前的Binder的類名表示,比如本例中的 “com.example.xiahao.IBookManger”
asInterface(android.os.IBinder obj)
用于將服務端的Binder對象轉換成客戶端所需要的AIDL接口的類型對象,這種轉換時區分進程的,如果客戶端和服務端位于同一進程,那么此方法返回的就是服務端的Stub對象本身,否則返回的時系統封裝后的Stub.proxy對象。
asBinder
此方法用于返回當前的Binder對象
onTransact
這個方法運行在服務端的Binder線程池中,當客戶端發起跨進程請求的時,遠程請求會通過系統底層封裝后交由此方法來處理。該方法的原型為
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
服務端通過code可以確定客戶端請求的目標方法是什么,接著從data中取出目標方法所需要的參數(如果目標方法有參數的話),然后執行目標方法,當目標方法執行完畢后,就向reply中寫入返回值(如果目標方法有返回值的話),onTransact方法的執行過程就是這樣的。需要注意的時,如果此方法返回false,那么客戶端的請求就會失敗,因此我們可以利用這個特性來做權限驗證,畢竟我們也不希望隨便一個進程都能遠程調用我們的服務。
Proxy#getBookList
這個方法運行在客戶端,當客戶端調用此方法時,它的內部實現是這樣的:首先創建該方法所需要的的輸入型Prcel對象 _data、輸出型Prcel對象 _reply和返回值對象List;然后把該方法的參數信息寫入 _data中(如果有參數的話);接著調用transact方法來發起RPC(遠程過程調用)請求,同時當前線程掛起;然后服務端onTransact方法會被調用,直到RPC過程返回后,當前線程繼續執行,并 _reply中取出RPC過程的返回結果;***返回 _reply中的數據。
Proxy#getBookList
這個方法運行在客戶端,它的執行過程和getBookList是一樣的,addBook沒有返回值,所以他不需要從 _reply中取出返回值。注意:當客戶端發起遠程請求時,由于當前線程會被掛起直至服務器返回數據,所以如果一個遠程的方法是很耗時的話,那么不能再UI線程中發起次遠程請求;其次,由于服務端的Binder方法運行在Binder的線程池中,所以Binder方法不管是否耗時都應該采用同步的方式實現,因為它已經運行在一個線程中了。為了更好的說明Binder,下面給出一個工作機制的圖:
接下來,介紹下Binder的兩個很重要的方法 linkTodeath 和 unlinkTodeath,如果服務端的Binder連接斷裂 (稱之為 Binder 死亡),會導致我們遠程調用失敗。更為關鍵的時,如果我們不知道Binder的連接已經斷裂,那么客戶端的功能就會受到影響。為此我們可以給Binder設置一個死亡代理,當Binder死亡時,我們就會收到通知,這個時候我們就可以給Binder設置一個死亡代理,這個時候就可以重新發起連接請求從而恢復連接。
聲明一個IBinder.DeathRecipient對象,IBinder.DeathRecipient是一個接口,其內部只有一個binderDied,我們需要實現這個方法,當binder死亡的時候,系統就會回調binderDied方法,然后我們就可以移除之前綁定的binder代理并重新綁定遠程服務:
//銷毀代理類,重啟服務 private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { Log.w(TAG, "binder: deed"); mIBinderPool.asBinder().unlinkToDeath(mDeathRecipient, 0); mIBinderPool = null; connectBinderPoolService(); } };
在客戶端綁定遠程服務成功后,給binder設置死亡代理
mIBinderPool = IBinderPool.Stub.asInterface(iBinder); try { mIBinderPool.asBinder().linkToDeath(mDeathRecipient,0); } catch (RemoteException e) { e.printStackTrace(); }
其中linkDeath的第二個參數是個標記位,我們直接設為0即可。經過上面的兩個步驟就給我們的binder設置了死亡代理,當binder死亡的時候我們就可以收到通知了。另外Binder的方法isBinderAlive也可以判斷Binder是否死亡。
Android中的IPC方式
使用Bundle
四大組件中的三大組件(Activity、Service、Receiver)都是支持Intent中傳遞Bundle數據的,由于Bundle實現了Pracel接口,所以它可以很方便的在不同的進程間傳輸。基于這一點,當我們調用了另一個進程中的Activity、Service、Receiver時,我們就可以在Bundle中附加我們需要傳輸給遠程進程的信息并通過Intent發生出去。當然,被傳輸的數據必須能夠被序列化,比如基本類型,實現了Pracelable、Serialzable接口以及一些Android支持的特殊對象。具體可以看Bundle這個類!
文件共享
兩個進程通過讀/寫同一個文件交換數據,比如A進程把數據寫入文件,B進程通過讀取這個文件來獲取數據。由于Android系統基于Linux,所以并發讀/寫文件沒有限制性,甚至兩個線程對同一個文件進行寫操作都是允許的,盡管這會出現問題,除了交換基本信息之外,我們可以序列化一個對象到文件系統中的同時從另一個進程恢復這個對象。文件共享的局限性是,并發讀/寫問題,如果并發讀/寫,讀出的數據可能不是***的,如果是并發寫的話就更嚴重了。
SharePreferences也屬于文件的一種,但是由于系統對它的讀/寫有一定的緩存策略,即在內存中會有一份SharePreferences文件的緩存,因此在多進程的情況下,系統對它的讀/寫變得不可靠,當面對高并發讀/寫數據就很有很大幾率丟失數據,不建議在進程間通信中使用SharePreferences。
Messenger
通過Messenger可以在不同進程中傳遞Message對象,在Message中放入我們需要傳入的數據,就可以實現數據的進程間傳遞了。Messenger是一種輕量級的IPC方案,它的底層實現是AIDL,并對AIDI做了封裝,使得可以很方便的進行進程間通信。同時,由于處理一個請求,因此在服務端我們不用考慮線程同步的問題。
服務端相關代碼
private static final String TAG = "MessengerService"; private static class MessengerServiceHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case Constants.MSG_FROM_CLIENT: Log.i(TAG, "server form client\t" + msg.getData().getString("msg")); Messenger client = msg.replyTo; Message replyMessage = Message.obtain(null, Constants.MSG_FROM_SERVICE); Bundle bundle = new Bundle(); bundle.putString("reply", "收到消息,我是服務端!"); replyMessage.setData(bundle); try { client.send(replyMessage); } catch (RemoteException e) { e.printStackTrace(); } break; default: super.handleMessage(msg); } } } private final Messenger mMessenger = new Messenger(new MessengerServiceHandler()); @Override public IBinder onBind(Intent intent) { return mMessenger.getBinder(); }
在AndroidManifest 配置服務 android:process=”:remote”
- 客戶端
private Messenger mMessenger; private static final String TAG = "MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Intent intent = new Intent(this, MessengerService.class); bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); } private ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mMessenger = new Messenger(service); Message message = Message.obtain(null, Constants.MSG_FROM_CLIENT); Bundle bundle = new Bundle(); bundle.putString("msg", "我是客戶端"); message.setData(bundle); //當客戶端發送消息的時候,需要把接受服務端回復的Messenger通過Message的replytTo參數傳遞給服務端。 message.replyTo = mGetReplyMessenger; try { mMessenger.send(message); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { } }; private Messenger mGetReplyMessenger = new Messenger(new MessengerHandler()); private static class MessengerHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case Constants.MSG_FROM_SERVICE: Log.i(TAG, "receive msg from Service\t" + msg.getData().getString("reply")); break; default: super.handleMessage(msg); } } } @Override protected void onDestroy() { super.onDestroy(); unbindService(mServiceConnection); } ```
在Messenger中進行數據傳遞必須將數據放入Message中,而Messenger和Message都是實現了Parcelable接口,因此可以跨進程傳輸。實際上:通過Messenger來傳輸Message,Message中能使用的載體只有what、arg1、arg2、Bundle已經replyTo。Message的另一個字段object在同一進程中是很實用的,但是在進程間通信時候,非系統的Parcelable對象無法通過object字段來傳輸。但可以實用Bundle,Bundle可以支持大量的數據類型。
使用AIDL
用Messenger來進行進程間通信時是以串行的方式處理客戶端發來的消息,如果大量的消息同時發送到服務端,服務端仍然只能一個個處理,如果大量的并發請求,那么用Messenger就不太合適了。同時Messenger的作用主要是為了傳遞消息,很多時候我們可能需要跨進程調用服務端方法,這個時候我們就可以用AIDL了。
大致步驟如下:
服務端
服務端首先要創建一個Service用來監聽客戶端的連接,然后創建一個AIDL文件,將暴露給客戶端的接口在這個AIDL文件中聲明,***在Service中實現這個AIDL文件即可。
客戶端
首先綁定服務端的Service,綁定成功后,將服務端返回的Binder對象轉成AIDL接口所屬的類型,接著就可以調用AIDL中的方法。
AIDL接口的創建
// IBookManager.aidl com.example.xiahao.myapplication; // Declare any non-default types here with import statements import com.example.xiahao.myapplication.Book; import com.example.xiahao.myapplication.IOnNewBookArrivedListener; interface IBookManager { List<Book> getBookList(); void addBook(in Book book); }
在AIDL文件中,并不是所有的類型都支持的,支持的類型如下:
- 基本數據類型(int 、long 、char、 boolean 、double等);
- string 和CharSequence
- List 只支持ArrayList,里面每個元素必須能夠被AIDL支持
- Map 只支持HashMap ,里面每個元素必須能夠被AIDL支持,包括key 和value
- Parcelable:所有實現了Parcelable接口的對象
- AIDL:所有AIDL接口本身也可以在AIDL文件中使用。
以上6中類型,其中自定義Parcelable對象和AIDL文件必須顯示的import進來,不管是否和當前的AIDL位于同一文件中。另外,如果AIDL用到了自定義的Parcelable對象必須新建一個和它同名的的AIDL文件,上面我們用到了Book,所以必須創建Book.aidl.
// Book.aidl package com.example.xiahao.myapplication; parcelable Book;
為了方便開發,建議把所以AIDL相關類和文件全部放入同一包中,當客戶端是另外一個應用時,我們可以直接把整個包復制到客戶端工程中。后面會給出一個書上的例子:具體包含,基本的AIDL調用,注冊解注冊,權限驗證,斷開重連,binder連接池一個服務處理多個AIDL的調用。
- 使用ContentProvide
ContentProvide專門用來應用之間的通訊,和Messenger一樣,ContentProvide底層也是Binder,雖然底層Binder但使用要比AIDL簡單多,因為系統幫我們做了封裝。
- 使用Socket
通過Socket進行進程間的通訊,它分為流式套接字和用戶數據套接字兩種,分別對應網絡協議層中的TCP和UDP協議。TCP是面向連接的協議,提供穩定的雙向的通訊功能,TCP的建立需要經過 “三次握手”才能完成,為提供穩定的的數據傳輸功能,其本身提供了超時重連機制,因此具有很高的穩定性。而UDP是無連接的,提供不穩定的單向的通訊功能,當然UDP也可以實現雙向通訊功能。在性能上,UDP具有更好的效率,其缺點就是不保證數據一定能夠正確傳輸,尤其是在網絡擁塞的情況下。
- 選用合適的IPC方式
給出書中的一張表格《Android開發藝術探討》
名稱 優點 缺點 適用場景
Bundle 簡單易用 只能傳輸Bundle支持的數據類型 四大組件的進程間通信
文件共享 簡單易用 不適合高并發場景,并且無法做到進程間的即時通訊 無并發訪問情形,交換簡單的數據實時性不高的場景
AIDL 功能強大,支持一對多并發通信,支持實時通信 使用稍復雜,需要處理好線程同步 一對多通信且RPC需要Messenger 功能一般,支持一對多串行通信,支持實時通信 不能很好的處理高并發情形,不支持RPC,數據通過Message進行傳輸,因此只能傳輸Bundle支持的數據類型 低并發的一對多即時通訊,無RPC需要,或者無需返回結果的RPC需求
ContentProvider 在數據訪問功能很強大,支持一對多并發數據共享,可通過call方法擴展其他操作 可以理解為受約束的AIDL,主要提供數據的CRUD操作 一對多的進程間的數據共享
Socket 功能強大,可以通過網絡傳輸字節流,并支持一對多并發實時通信 實現細節稍微有點繁瑣,不支持直接的RPC 網絡數據交換
關于Android中IPC機制的原理是什么問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。