巧用Android多程式,微信,微博等主流App都在用
1
前言
對於程式的概念,來到這裡的都是程式設計修仙之人,就不再囉嗦了,相信大家倒著、跳著、躺著、各種姿勢都能背出來。
2
為什麼使用多程式?
相信很多同學在實際開發中,基本都不會去給app劃分程式,而且,在Android中使用多程式,還可能需要編寫額外的程式通訊程式碼,還可能帶來額外的Bug,這無疑加大了開發的工作量,在很多創業公司中工期也不允許,這導致了整個app都在一個程式中。
整個app都在一個程式有什麼弊端?
在Android中,虛擬機器分配給各個程式的執行記憶體是有限制值的(這個值可以是32M,48M,64M等,根據機型而定),試想一下,如果在app中,增加了一個很常用的圖片選擇模組用於上傳圖片或者頭像,載入大量Bitmap會使app的記憶體佔用迅速增加,如果你還把檢視過的圖片快取在了記憶體中,那麼OOM的風險將會大大增加,如果此時還需要使用WebView載入一波網頁,我就問你怕不怕!
微信,微博等主流app是如何解決這些問題的?
微信移動開發團隊在 《Android記憶體優化雜談》 一文中就說到:“對於webview,相簿等,由於存在記憶體系統洩露或者佔用記憶體過多的問題,我們可以採用單獨的程式。微信當前也會把它們放在單獨的tools程式中”。
下面我們使用adb檢視一下微信和微博的程式資訊(Android 5.0以下版本可直接在“設定 -> 應用程式”相關條目中檢視):
進入adb shell後,使用 “ps | grep 條目名稱” 可以過濾出想要檢視的程式。
可以看到,微信的確有一個tools程式,而新浪微博也有image相關的程式,而且它們當中還有好些其它的程式,比如微信的push程式,微博的remote程式等,這裡可以看出,他們不單單只是把上述的WebView、相簿等放到單獨的程式,還有推送服務等也是執行在獨立的程式中的。一個訊息推送服務,為了保證穩定性,可能需要和UI程式分離,分離後即使UI程式退出、Crash或者出現記憶體消耗過高等情況,仍不影響訊息推送服務。
可見,合理使用多程式不僅僅是有多大好處的問題,我個人認為而且是很有必要的。
所以說,我們最好還是根據自身情況,考慮一下是否需要拆分程式。這也是本文的初衷:給大家提供一個多程式的參考思路,在遇到上述問題和場景的時候,可以考慮用多程式的方法來解決問題,又或者,在面試的時候,跟面試官聊到這方面的知識時候也不至於尷尬。
3
為什麼需要“跨程式通訊”?
Android的程式與程式之間通訊,有些不需要我們額外編寫通訊程式碼,例如:把選擇圖片模組放到獨立的程式,我們仍可以使用startActivityForResult方法,將選中的圖片放到Bundle中,使用Intent傳遞即可。(看到這裡,你還不打算把你專案的圖片選擇弄到獨立程式麼?)
但是對於把“訊息推送Service”放到獨立的程式,這個業務就稍微複雜點了,這個時候可能會發生Activity跟Service傳遞物件,呼叫Service方法等一系列複雜操作。
由於各個程式執行在相對獨立的記憶體空間,所以它們是不能直接通訊的,因為程式裡的變數、物件等初始化後都是具有記憶體地址的,舉個簡單的例子,讀取一個變數的值,本質是找到變數的記憶體地址,取出存放的值。不同的程式,執行在相互獨立的記憶體(其實就可以理解為兩個不同的應用程式),顯然不能直接得知對方變數、物件的記憶體地址,這樣的話也自然不能訪問對方的變數,物件等。此時兩個程式進行互動,就需要使用跨程式通訊的方式去實現。簡單說,跨程式通訊就是一種讓程式與程式之間可以進行互動的技術。
4
跨程式通訊的方式有哪些?
1、四大元件間傳遞Bundle;
2、使用檔案共享方式,多程式讀寫一個相同的檔案,獲取檔案內容進行互動;
3、使用Messenger,一種輕量級的跨程式通訊方案,底層使用AIDL實現(實現比較簡單,博主開始本文前也想了一下是否要說一下這個東西,最後還是覺得沒有這個必要,Google一下就能解決的問題,就不囉嗦了);
4、使用AIDL(Android Interface Definition Language),Android介面定義語言,用於定義跨程式通訊的介面;
5、使用ContentProvider,常用於多程式共享資料,比如系統的相簿,音樂等,我們也可以通過ContentProvider訪問到;
6、使用Socket傳輸資料。
接下來本文將重點介紹使用AIDL進行多程式通訊,因為AIDL是Android提供給我們的標準跨程式通訊API,非常靈活且強大(貌似面試也經常會問到,但是真正用到的也不多…)。上面所說的Messenger也是使用AIDL實現的一種跨程式方式,Messenger顧名思義,就像是一種序列的訊息機制,它是一種輕量級的IPC方案,可以在不同程式中傳遞Message物件,我們在Message中放入需要傳遞的資料即可輕鬆實現程式間通訊。但是當我們需要呼叫服務端方法,或者存在併發請求,那麼Messenger就不合適了。而四大元件傳遞Bundle,這個就不需要解釋了,把需要傳遞的資料,用Intent封裝起來傳遞即可,其它方式不在本文的討論範圍。
下面開始對AIDL的講解,各位道友準備好渡劫了嗎?
5
使用AIDL實現一個多程式訊息推送
像圖片選擇這樣的多程式需求,可能並不需要我們額外編寫程式通訊的程式碼,使用四大元件傳輸Bundle就行了,但是像推送服務這種需求,程式與程式之間需要高度的互動,此時就繞不過程式通訊這一步了。
下面我們就用即時聊天軟體為例,手動去實現一個多程式的推送例子,具體需求如下:1、UI和訊息推送的Service分兩個程式;
2、UI程式用於展示具體的訊息資料,把使用者傳送的訊息,傳遞到訊息Service,然後傳送到遠端伺服器;
3、Service負責收發訊息,並和遠端伺服器保持長連線,UI程式可通過Service傳送訊息到遠端伺服器,Service收到遠端伺服器訊息通知UI程式;
4、即使UI程式退出了,Service仍需要保持執行,收取伺服器訊息。
6
實現思路
先來整理一下實現思路:
1、建立UI程式(下文統稱為客戶端);
2、建立訊息Service(下文統稱為服務端);
3、把服務端配置到獨立的程式(AndroidManifest.xml中指定process標籤);
4、客戶端和服務端進行繫結(bindService);
5、讓客戶端和服務端具備互動的能力。(AIDL使用);
7
例子具體實現
為了閱讀方便,下文中程式碼將省略非重點部分,可以把本文完整程式碼Clone到本地再看文章:
https://github.com/V1sk/AIDL
Step0. AIDL呼叫流程概覽
開始之前,我們先來概括一下使用AIDL進行多程式呼叫的整個流程:
1、客戶端使用bindService方法繫結服務端;
2、服務端在onBind方法返回Binder物件;
3、客戶端拿到服務端返回的Binder物件進行跨程式方法呼叫;
整個AIDL呼叫過程概括起來就以上3個步驟,下文中我們使用上面描述的例子,來逐步分解這些步驟,並講述其中的細節。
Step1.客戶端使用bindService方法繫結服務端
1.1
建立客戶端和服務端,把服務端配置到另外的程式
1、建立客戶端 -> MainActivity;
2、建立服務端 -> MessageService;
3、把服務端配置到另外的程式 -> android:process=”:remote”
上面描述的客戶端、服務端、以及把服務端配置到另外程式,體現在AndroidManifest.xml中,如下所示:
開啟多程式的方法很簡單,只需要給四大元件指定android:process標籤。
1.2
繫結MessageService到MainActivity
建立MessageService:
此時的MessageService就是剛建立的模樣,onBind中返回了null,下一步中我們將返回一個可操作的物件給客戶端。
客戶端MainActivity呼叫bindService方法繫結MessageService
這一步其實是屬於Service元件相關的知識,在這裡就比較簡單地說一下,啟動服務可以通過以下兩種方式:
1、使用bindService方法 -> bindService(Intent service, ServiceConnection conn, int flags);
2、使用startService方法 -> startService(Intent service);
bindService & startService區別:
使用bindService方式,多個Client可以同時bind一個Service,但是當所有Client unbind後,Service會退出,通常情況下,如果希望和Service互動,一般使用bindService方法,使用onServiceConnected中的IBinder物件可以和Service進行互動,不需要和Service互動的情況下,使用startService方法即可。正如上面所說,我們是要和Service互動的,所以我們需要使用bindService方法,但是我們希望unbind後Service仍保持執行,這樣的情況下,可以同時呼叫bindService和startService(比如像本例子中的訊息服務,退出UI程式,Service仍需要接收到訊息),程式碼如下:
Stpe2.服務端在onBind方法返回Binder物件
2.1
首先,什麼是Binder?
要說Binder,首先要說一下IBinder這個介面,IBinder是遠端物件的基礎介面,輕量級的遠端過程呼叫機制的核心部分,該介面描述了與遠端物件互動的抽象協議,而Binder實現了IBinder介面,簡單說,Binder就是Android SDK中內建的一個多程式通訊實現類,在使用的時候,我們不用也不要去實現IBinder,而是繼承Binder這個類即可實現多程式通訊。
2.2
其次,這個需要在onBind方法返回的Binder物件從何而來?
在這裡就要引出本文中的主題了——AIDL
多程式中使用的Binder物件,一般通過我們定義好的 .adil 介面檔案自動生成,當然你可以走野路子,直接手動編寫這個跨程式通訊所需的Binder類,其本質無非就是一個繼承了Binder的類,鑑於野路子走起來麻煩,而且都是重複步驟的工作,Google提供了 AIDL 介面來幫我們自動生成Binder這條正路,下文中我們圍繞 AIDL 這條正路繼續展開討論(可不能把人給帶偏了是吧)2.3
定義AIDL介面
很明顯,接下來我們需要搞一波上面說的Binder,讓客戶端可以呼叫到服務端的方法,而這個Binder又是通過AIDL介面自動生成,那我們就先從AIDL搞起,搞之前先看看注意事項,以免出事故:
AIDL支援的資料型別:
-
Java 程式語言中的所有基本資料型別(如 int、long、char、boolean 等等)
-
String和CharSequence
-
Parcelable:實現了Parcelable介面的物件
-
List:其中的元素需要被AIDL支援,另一端實際接收的具體類始終是 ArrayList,但生成的方法使用的是 List 介面
-
Map:其中的元素需要被AIDL支援,包括 key 和 value,另一端實際接收的具體類始終是 HashMap,但生成的方法使用的是 Map 介面
其他注意事項:
-
在AIDL中傳遞的物件,必須實現Parcelable序列化介面;
-
在AIDL中傳遞的物件,需要在類檔案相同路徑下,建立同名、但是字尾為.aidl的檔案,並在檔案中使用parcelable關鍵字宣告這個類;
-
跟普通介面的區別:只能宣告方法,不能宣告變數;
-
所有非基礎資料型別引數都需要標出資料走向的方向標記。可以是 in、out 或 inout,基礎資料型別預設只能是 in,不能是其他方向。
下面繼續我們的例子,開始對AIDL的講解~
2.4
建立一個AIDL介面,介面中提供傳送訊息的方法
(Android Studio建立AIDL:專案右鍵 -> New -> AIDL -> AIDL File),程式碼如下:
一個比較尷尬的事情,看了很多文章,從來沒有一篇能說清楚in、out、inout這三個引數方向的意義,後來在stackoverflow上找到能理解答案(https://stackoverflow.com/questions/4700225/in-out-inout-in-a-aidl-interface-parameter-value),我翻譯一下大概意思:
被“in”標記的引數,就是接收實際資料的引數,這個跟我們普通引數傳遞一樣的含義。在AIDL中,“out” 指定了一個僅用於輸出的引數,換而言之,這個引數不關心呼叫方傳遞了什麼資料過來,但是這個引數的值可以在方法被呼叫後填充(無論呼叫方傳遞了什麼值過來,在方法執行的時候,這個引數的初始值總是空的),這就是“out”的含義,僅用於輸出。而“inout”顯然就是“in”和“out”的合體了,輸入和輸出的引數。區分“in”、“out”有什麼用?這是非常重要的,因為每個引數的內容必須編組(序列化,傳輸,接收和反序列化)。in/out標籤允許Binder跳過編組步驟以獲得更好的效能。
上述的MessageModel為訊息的實體類,該類在AIDL中傳遞,實現了Parcelable序列化介面,程式碼如下:
手動實現Parcelable介面比較麻煩,安利一款AS自動生成外掛android-parcelable-intellij-plugin
建立完MessageModel這個實體類,別忘了還有一件事要做:”在AIDL中傳遞的物件,需要在類檔案相同路徑下,建立同名、但是字尾為.aidl的檔案,並在檔案中使用parcelable關鍵字宣告這個類“。程式碼如下:
對於沒有接觸過aidl的同學,光說就能讓人懵逼,來看看此時的專案結構壓壓驚:
我們剛剛新增的3個檔案:
-
MessageSender.aidl -> 定義了傳送訊息的方法,會自動生成名為MessageSender.Stub的Binder類,在服務端實現,返回給客戶端呼叫
-
MessageModel.java -> 訊息實體類,由客戶端傳遞到服務端,實現了Parcelable序列化
-
MessageModel.aidl -> 宣告瞭MessageModel可在AIDL中傳遞,放在跟MessageModel.java相同的包路徑下
OK,相信此時懵逼已解除~
2.5
在服務端建立MessageSender.aidl這個AIDL介面自動生成的Binder物件,並返回給客戶端呼叫,服務端MessageService程式碼如下:
MessageSender.Stub是Android Studio根據我們MessageSender.aidl檔案自動生成的Binder物件(至於是怎樣生成的,下文會有答案),我們需要把這個Binder物件返回給客戶端。
2.6
客戶端拿到Binder物件後呼叫遠端方法
呼叫步驟如下:
1、在客戶端的onServiceConnected方法中,拿到服務端返回的Binder物件;
2、使用MessageSender.Stub.asInterface方法,取得MessageSender.aidl對應的操作介面;
3、取得MessageSender物件後,像普通介面一樣呼叫方法即可。
此時客戶端程式碼如下:
在客戶端中我們呼叫了MessageSender的sendMessage方法,向服務端傳送了一條訊息,並把生成的MessageModel物件作為引數傳遞到了服務端,最終服務端列印的結果如下:
這裡有兩點要說:
1、服務端已經接收到客戶端傳送過來的訊息,並正確列印;
2、服務端和客戶端區分兩個程式,PID不一樣,程式名也不一樣;
到這裡,我們已經完成了最基本的使用AIDL進行跨程式方法呼叫,也是Step.0的整個細化過程,可以再回顧一下Step.0,既然已經學會使用了,接下來…全劇終。。。
如果寫到這裡全劇終,那跟鹹魚有什麼區別…
8
知其然,知其所以然
我們通過上述的呼叫流程,看看從客戶端到服務端,都經歷了些什麼事,看看Binder的上層是如何工作的,至於Binder的底層,這是一個非常複雜的話題,本文不深究。(如果看到這裡你又想問什麼是Binder的話,請手動倒帶往上看…)
我們先來回顧一下從客戶端發起的呼叫流程:
1、MessageSender messageSender = MessageSender.Stub.asInterface(service);
2、messageSender.sendMessage(messageModel);
拋開其它無關程式碼,客戶端調跨程式方法就這兩個步驟,而這兩個步驟都封裝在 MessageSender.aidl 最終生成的 MessageSender.java 原始碼(具體路徑為:build目錄下某個子目錄,自己找,不爽你來打我啊 �� )
請看下方程式碼和註釋,前方高能預警…
只看程式碼的話,可能會有點懵逼,相信結合程式碼再看下方的流程圖會更好理解:
從客戶端的sendMessage開始,整個AIDL的呼叫過程如上圖所示,asInterface方法,將會判斷onBind方法返回的Binder是否存處於同一程式,在同一程式中,則進行常規的方法呼叫,若處於不同程式,整個資料傳遞的過程則需要通過Binder底層去進行編組(序列化,傳輸,接收和反序列化),得到最終的資料後再進行常規的方法呼叫。
敲黑板:物件跨程式傳輸的本質就是 序列化,傳輸,接收和反序列化 這樣一個過程,這也是為什麼跨程式傳輸的物件必須實現Parcelable介面
9
跨程式的回撥介面
在上面我們已經實現了從客戶端傳送訊息到跨程式服務端的功能,接下來我們還需要將服務端接收到的遠端伺服器訊息,傳遞到客戶端。有同學估計會說:“這不就是一個回撥介面的事情嘛”,設定回撥介面思路是對的,但是在這裡使用的回撥介面有點不一樣,在AIDL中傳遞的介面,不能是普通的介面,只能是AIDL介面,所以我們需要新建一個AIDL介面傳到服務端,作為回撥介面。
新建訊息收取的AIDL介面MessageReceiver.aidl:
接下來我們把回撥介面註冊到服務端去,修改我們的MessageSender.aidl:
以上就是我們最終修改好的aidl介面,接下來我們需要做出對應的變更:
1、在服務端中增加MessageSender的註冊和反註冊介面的方法;
2、在客戶端中實現MessageReceiver介面,並通過MessageSender註冊到服務端。
客戶端變更,修改MainActivity:
客戶端主要有3個變更:
1、增加了messageReceiver物件,用於監聽服務端的訊息通知;
2、onServiceConnected方法中,把messageReceiver註冊到Service中去;
3、onDestroy時候解除messageReceiver的註冊。
下面對服務端MessageServie進行變更:
服務端主要變更:
1、MessageSender.Stub實現了註冊和反註冊回撥介面的方法;
2、增加了RemoteCallbackList來管理AIDL遠端介面;
3、FakeTCPTask模擬了長連線通知客戶端有新訊息到達。(這裡的長連線可以是XMPP,Mina,Mars,Netty等,這裡弄個假的意思意思,有時間的話我們開個帖子聊聊XMPP)
這裡還有一個需要講一下的,就是RemoteCallbackList,為什麼要用RemoteCallbackList,普通ArrayList不行嗎?當然不行,不然幹嘛又整一個RemoteCallbackList ��,registerReceiveListener 和 unregisterReceiveListener在客戶端傳輸過來的物件,經過Binder處理,在服務端接收到的時候其實是一個新的物件,這樣導致在 unregisterReceiveListener 的時候,普通的ArrayList是無法找到在 registerReceiveListener 時候新增到List的那個物件的,但是它們底層使用的Binder物件是同一個,RemoteCallbackList利用這個特性做到了可以找到同一個物件,這樣我們就可以順利反註冊客戶端傳遞過來的介面物件了。RemoteCallbackList在客戶端程式終止後,它能自動移除客戶端所註冊的listener,它內部還實現了執行緒同步,所以我們在註冊和反註冊都不需要考慮執行緒同步,的確是個666的類。(至於使用ArrayList的么蛾子現象,大家可以自己試試,篇幅問題,這裡就不演示了)
到此,服務端通知客戶端相關的程式碼也寫完了,執行結果無非就是正確列印��就不貼圖了,可以自己Run一下,列印的時候注意去選擇不同的程式,不然瞪壞螢幕也沒有日誌。
10
DeathRecipent
你以為這樣就完了?too young too simple…
不知道你有沒有感覺到,兩個程式互動總是覺得缺乏那麼一點安全感…比如說服務端程式Crash了,而客戶端程式想要呼叫服務端方法,這樣就呼叫不到了。此時我們可以給Binder設定一個DeathRecipient物件,當Binder意外掛了的時候,我們可以在DeathRecipient介面的回撥方法中收到通知,並作出相應的操作,比如重連服務等等。
DeathRecipient的使用如下:
1、宣告DeathRecipient物件,實現其binderDied方法,當binder死亡時,會回撥binderDied方法;
2、給Binder物件設定DeathRecipient物件。
在客戶端MainActivity宣告DeathRecipient:
Binder中兩個重要方法:
1、linkToDeath -> 設定死亡代理 DeathRecipient 物件;
2、unlinkToDeath -> Binder死亡的情況下,解除該代理。
此外,Binder中的isBinderAlive也可以判斷Binder是否死亡。
11
許可權驗證
就算是公交車,上車也得嘀卡對不,如果希望我們的服務程式不想像公交車一樣誰想上就上,那麼我們可以加入許可權驗證。
介紹兩種常用驗證方法:
1、在服務端的onBind中校驗自定義permission,如果通過了我們的校驗,正常返回Binder物件,校驗不通過返回null,返回null的情況下客戶端無法繫結到我們的服務;
2、在服務端的onTransact方法校驗客戶端包名,不通過校驗直接return false,校驗通過執行正常的流程。
自定義permission,在Androidmanifest.xml中增加自定義的許可權:
服務端檢查許可權的方法:
12
根據不同程式,做不同的初始化工作
相信前一兩年很多朋友還在使用Android-Universal-Image-Loader來載入圖片,它是需要在Application類進行初始化的。打個比如,我們用它來載入圖片,而且還有一個圖片選擇程式,那麼我們希望分配更多的快取給圖片選擇程式,又或者是一些其他的初始化工作,不需要在圖片選擇程式初始化怎麼辦?
這裡提供一個簡單粗暴的方法,博主也是這麼幹的…直接拿到程式名判斷,作出相應操作即可:
每個程式建立,都會呼叫Application的onCreate方法,這是一個需要注意的地方,我們也可以根據當前程式的pid,拿到當前程式的名字去做判斷,然後做一些我們需要的邏輯,我們這個例子,拿到的兩個程式名分別是:
1、客戶端程式:com.example.aidl
2、服務端程式:com.example.aidl:remote
13
總結
1、多程式app可以在系統中申請多份記憶體,但應合理使用,建議把一些高消耗但不常用的模組放到獨立的程式,不使用的程式可及時手動關閉;
2、實現多程式的方式有多種:四大元件傳遞Bundle、Messenger、AIDL等,選擇適合自己的使用場景;
3、Android中實現多程式通訊,建議使用系統提供的Binder類,該類已經實現了多程式通訊而不需要我們做底層工作;
4、多程式應用,Application將會被建立多次;
14
結語
這篇文章斷斷續續寫了很久,而且我相信真正使用起來的同學可能不多,選擇這樣一個話題我是吃力不討好… 但是我還是希望可以在這裡提供一個完整的解決方案給大家。簡單的多程式使用,而且效果顯著的,比如把圖片選擇和WebView配置到獨立的程式,這個我希望可以大家行動起來。這篇文章的知識點非常多,理解起來可能不是太容易,如果有興趣,我建議你手動去寫一下,然後不理解的地方,打斷點看看是什麼樣的執行步驟。
對於面試的同學,如果在面試過程中說到多程式,跟面試官聊得開,估計也是能加點分的,或者在實際工作中,一些使用多程式可以更好地解決問題的地方,你可以在會議中拍桌猛起,跟主管說:“我有一個大膽的想法…”,這樣裝逼也不錯(當然,被炒了的話就不關我的事了…)
相關文章
- 新浪微博app如何去喚起跳轉微信小程式APP微信小程式
- 讓微信等知名 APP 都中招的 XcodeGhost 事件全面詳細回顧APPXCode事件
- android appium微信等自動化的那些坑兒AndroidAPP
- 淘寶、微博、微信的 Android 圖片放置策略Android
- 突破Android微信微博瀏覽器限制直接拉起應Android瀏覽器
- js在微信、微博、QQ、Safari喚起App的解決方案JSAPP
- JS 在微信、微博、QQ、Safari 喚起 App 的解決方案JSAPP
- 【Swift】類似於微博、微信的多圖瀏覽 檢視Swift
- Android 無需申請key直接呼叫微信/QQ/微博分享Android
- 用 mpvue 寫個【微博-青銅版】微信小程式Vue微信小程式
- 微博和微信的區別
- Android社交登入授權、分享SDK,支援微信、微博和QQAndroid
- 巧用Koa接管“對接微信開發”的工作 - 多使用者微信JS-SDK API服務JSAPI
- Flutter 仿微信/微博九宮格Flutter
- 侃一侃主流的程式語言都各自有何特點?
- 為什麼主流的 App 看起來都差不多?這可能是件好事APP
- 使用Chrome開發者工具除錯Android端內網頁(微信,QQ,UC,App內嵌頁等)Chrome除錯Android內網網頁APP
- 如何快速上手百度微博都在用的Web框架Yaf?Web框架
- 微信app支付 java後臺接AndroidAPPJavaAndroid
- 微信分享網頁連結至朋友、朋友圈、微博程式碼網頁
- 【Swift】類似於微博、微信的ActionSheetSwift
- uniapp 微信小程式總結APP微信小程式
- 玩Android微信小程式版Android微信小程式
- HTTP/3 都來了,你卻還在用 HTTP/1.1?HTTP
- uni-app 1.2釋出,iOS、Android、小程式、H5主流四端全覆蓋APPiOSAndroidH5
- JavaScript、PHP、Python等主流程式語言爆安全漏洞JavaScriptPHPPython
- uniapp,微信小程式中使用 MQTTAPP微信小程式MQQT
- Appium 之測試微信小程式APP微信小程式
- Appium之測試微信小程式APP微信小程式
- uniapp微信小程式獲取定位APP微信小程式
- DailyView:十大臺灣民眾愛用的大陸APP 抖音、微博、微信位列前三AIViewAPP
- android --巧用 flexboxLayout 佈局AndroidFlex
- Linux 主流發行版本,你都知道哪個?Linux
- Android 多程式通訊Android
- 微信配置Universal Link解決方案支援多個appAPP
- 微信小程式和app最大區別在哪微信小程式APP
- uni-app 微信小程式全域性分享APP微信小程式
- 每個Android迷都應該使用的75個超酷AppAndroidAPP