前言
今天看到一道騰訊面試題,關於Android多程序,那麼今天就來聊聊吧。
Android中建立多程序的方式1) 第一種,大家熟知的,就是給四大元件再AndroidManifest中指定android:process屬性。
<activity android:name="com.example.uithread.UIActivity" android:process=":test"/> <activity android:name="com.example.uithread.UIActivity2" android:process="com.example.test"/>
可以看到,android:process有兩種表達方式:
:test。“:”的含義是指要在當前的程序名前面加上當前的包名,如果當前包名為com.example.jimu。那麼這個程序名就應該是com.example.jimu:test。這種冒號開頭的程序屬於當前應用的私有程序,其他應用的元件不可以和他跑到同一個程序中。com.example.test。第二種表達方式,是完整的命名方式,它就是新程序的程序名,這種屬於全域性程序,其他應用可以透過shareUID的方式跑到同一個程序中。簡單說下shareUID:正常來說,Android中每個app都是一個單獨的程序,與之對應的是一個唯一的linux user ID,所以就能保住該應用程式的檔案或者元件只對該應用程式可見。但是也有一個辦法能讓不同的apk進行共享檔案,那就是透過shareUID,它可以使不同的apk使用相同的 user ID。貼下用法:
//app1<manifest package="com.test.app1"android:sharedUserId="com.test.jimu">//app2<manifest package="com.test.app2"android:sharedUserId="com.test.jimu">//app1中獲取app2的上下文:Context mContext=this.createPackageContext("com.test.app2", Context.CONTEXT_IGNORE_SECURITY);
2)第二種建立程序的方法,就是透過JNI在native層中去fork一個程序。
這種就比較複雜了,我在網上找了一些資料,找到一個fork普通程序的:
//主要程式碼long add(long x,long y) { //fpid表示fork函式返回的值 pid_t fpid; int count=0; fpid=fork(); }//結果:USER PID PPID VSZ RSS STAT NAME root 152 1 S zygoteu0_a66 17247 152 297120 44096 S com.example.jniu0_a66 17520 17247 0 0 Z com.example.jni
最終的結果是可以創建出一個程序,但是沒有執行,佔用的記憶體為0,處於殭屍程式狀態。
但是它這個是透過普通程序fork出來的,我們知道Android中所有的程序都是直接透過zygote程序fork出來的(fork可以理解為孵化出來的當前程序的一個副本)。所以不知道直接去操作zygote程序可不可以成功,有了解的小夥伴可以在微信討論群裡給大家說說。
對了,有的小夥伴可能會問,為什麼所有程序都必須用zygote程序fork呢?
這是因為fork的行為是複製整個使用者的空間資料以及所有的系統物件,並且只複製當前所在的執行緒到新的程序中。也就是說,父程序中的其他執行緒在子程序中都消失了,為了防止出現各種問題(比如死鎖,狀態不一致)呢,就只讓zygote程序,這個單執行緒的程序,來fork新程序。而且在zygote程序中會做好一些初始化工作,比如啟動虛擬機器,載入系統資源。這樣子程序fork的時候也就能直接共享,提高效率,這也是這種機制的優點。一個應用使用多程序會有什麼問題嗎?上面說到建立程序的方法很簡單,寫個android:process屬性即可,那麼使用是不是也這麼簡單呢?很顯然不是,一個應用中多程序會導致各種各樣的問題,主要有如下幾個:
靜態成員和單例模式完全失效。因為每個程序都會分配到一個獨立的虛擬機器,而不同的虛擬機器在記憶體分配上有不同的地址空間,所以在不同的程序,也就是不同的虛擬機器中訪問同一個類的物件會產生多個副本。執行緒同步機制完全失效。同上面一樣,不同的記憶體是無法保證執行緒同步的,因為執行緒鎖的物件都不一樣了。SharedPreferences不在可靠。之前有一篇說SharedPreferences的文章中說過這一點,SharedPreferences是不支援多程序的。Application會多次建立。多程序其實就對應了多應用,所以新程序建立的過程其實就是啟動了一個新的應用,自然也會建立新的Application,Application和虛擬機器和一個程序中的元件是一一對應的。Android中的IPC方式既然多程序有很多問題,自然也就有解決的辦法,雖然不能共享記憶體,但是可以進行資料互動啊,也就是可以進行多程序間通訊,簡稱IPC。
下面就具體說說Android中的八大IPC方式:
1. Bundle Android四大元件都是支援在Intent中使用Bundle來傳遞資料,所以四大元件直接的程序間通訊就可以使用Bundle。但是Bundle有個大小限制要注意下,bundle的資料傳遞限制大小為1M,如果你的資料超過這個大小就要使用其他的通訊方式了。
2. 檔案共享 這種方式就是多個程序透過讀寫一個檔案來交換資料,完成程序間通訊。但是這種方式有個很大的弊端就是多執行緒讀寫容易出問題,也就是併發問題,如果出現併發讀或者併發寫都容易出問題,所以這個方法適合對資料同步要求不高的程序直接進行通訊。
這裡可能有人就奇怪了,SharedPreference不就是讀寫xml檔案嗎?怎麼就不支援程序間通訊了?
這是因為系統對於SharedPreference有讀寫快取策略,也就是在記憶體中有一份SharedPreference檔案的快取,涉及到記憶體了,那肯定在多程序中就不那麼可靠了。3. MessengerMessenger是用來傳遞Message物件的,在Message中可以放入我們要傳遞的資料。它是一種輕量級的IPC方案,底層實現是AIDL。
看看用法,客戶端和服務端通訊:
//客戶端public class MyActivity extends Activity { private Messenger mService; private Messenger mGetReplyMessager=new Messenger(new MessengerHandler()); private static class MessengerHandler extends Handler{ @Override public void handleMessage(@NonNull Message msg) { switch (msg.what){ case 1: //收到訊息 break; } super.handleMessage(msg); } } private ServiceConnection mConnection=new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = new Messenger(service); Message msg = Message.obtain(null,0); Bundle bundle = new Bundle(); bundle.putString("test", "message1"); msg.setData(bundle); msg.replyTo = mGetReplyMessager; //設定接受訊息者 try { mService.send(msg); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { } }; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Intent i=new Intent(this,ServerService.class); bindService(i,mConnection, Context.BIND_AUTO_CREATE); } @Override protected void onDestroy() { super.onDestroy(); unbindService(mConnection); }}//服務端 public class ServerService extends Service { Messenger messenger = new Messenger(new MessageHandler()); public ServerService() { } public IBinder onBind(Intent intent) { return messenger.getBinder(); } private class MessageHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what){ case 0: //收到訊息併發送訊息 Messenger replyTo = msg.replyTo; Message replyMsg = Message.obtain(null,1); Bundle bundle = new Bundle(); bundle.putString("test","111"); replyMsg.setData(bundle); try { replyTo.send(replyMsg); } catch (RemoteException e) { e.printStackTrace(); } break; } super.handleMessage(msg); } }}
4. AIDL
Messenger雖然可以傳送訊息和接收訊息,但是無法同時處理大量訊息,並且無法跨程序方法。但是AIDL則可以做到,這裡簡單說下AIDL的使用流程:
服務端首先建立一個Service監聽客戶端的連線請求,然後建立一個AIDL檔案,將暴露給客戶端的介面在這個AIDL檔案中申明,最後在Service中實現這個AIDL介面。
客戶端需要繫結這個服務端的Service,然後將服務端返回的Binder物件轉換成AIDL介面的屬性,然後就可以呼叫AIDL中的方法了。
5. ContentProvider
這個大家應很熟悉了,四大元件之一,專門用於不同應用間進行資料共享的。它的底層實現是透過Binder實現的。主要使用方法有兩步:
1、宣告Provider
<provider android:authorities="com.test.lz" //provider的唯一標識 android:name=".BookProdiver" android:permission="com.test.permission" android:process=":provider"/>
android:authorities,唯一標識,一般用包名。外界在訪問資料的時候都是透過uri來訪問的,uri由四部分組成content://com.test.lz/ table/ 100| | | |固定欄位 Authority 資料表名 資料ID
android:permission,許可權屬性,還有readPermission,writePermission。如果provider聲明瞭許可權相關屬性,那麼其他應用也必須宣告相應的許可權才能進行讀寫操作。比如:
//應用1<permission android:name="me.test.permission" android:protectionLevel="normal"/> <provider android:authorities="com.test.lz" android:name=".BookProdiver" android:permission="com.test.permission" android:process=":provider"/> //應用2<uses-permission android:name="me.test.permission"/>
2、然後外界應用透過getContentResolver()的增刪查改方法訪問資料即可。
6. Socket
套接字,在網路通訊中用的很多,比如TCP,UDP。關於Socket通訊,借用網路上的一張圖說明:
然後簡單貼下關鍵程式碼:
//申請許可權<uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />//服務端ServerSocket serverSocket = new ServerSocket(8688);while(isActive) { try { //不斷獲取客戶端連線 final Socket client = serverSocket.accept(); new Thread(){ @Override public void run() { try { //處理訊息 } catch (IOException e) { e.printStackTrace(); } } }.start(); } catch (IOException e) { e.printStackTrace(); } }//客戶端Socket socket = null; while(socket==null){ //為空代表連結失敗,重連 try { socket = new Socket("localhost",8688); out = new PrintWriter(socket.getOutputStream(),true); handler.sendEmptyMessage(1); final Socket finalSocket = socket; new Thread(){ @Override public void run() { try { reader = new BufferedReader(new InputStreamReader(finalSocket.getInputStream())); } catch (IOException e) { e.printStackTrace(); } while(!MainActivity.this.isFinishing()){ //迴圈讀取訊息 try { String msg = reader.readLine(); } catch (IOException e) { e.printStackTrace(); } } } }.start(); } catch (IOException e) { e.printStackTrace(); } }
7. Binder連線池
關於Binder的介紹,之前的文章已經說過了。這裡主要講一個Binder的實際使用的技術——Binder連線池。由於每個AIDL請求都要開啟一個服務,防止太多服務被建立,就引用了Binder連線池技術。Binder連線池的主要作用就是將每個業務模組的Binder請求統一 轉發到遠端Service中去執行,從而避免了重複建立Service的過程。貼一下Binder連線池的工作原理:
每個業務模組建立自己的AIDL介面並實現此介面,然後向服務端提供自己的唯一標識和其對應的Binder物件.對於服務端來說,只需要一個 Service就可以了,服務端提供一個queryBinder介面,這個介面能夠根據業務模組的特徵來 返回相應的Binder物件給它們,不同的業務模組拿到所需的Binder物件後就可以進行遠端方法呼叫了。具體怎麼用呢?還是簡單貼下關鍵原始碼
public class BinderPoolImpl extends IBinderPool.Stub { private static final String TAG = "Service BinderPoolImpl"; public BinderPoolImpl() { super(); } @Override public IBinder queryBinder(int binderCode) throws RemoteException { Log.e(TAG, "binderCode = " + binderCode); IBinder binder = null; switch (binderCode) { case 0: binder = new SecurityCenterImpl(); break; case 1: binder = new ComputeImpl(); break; default: break; } return binder; }}public class BinderPool { //... private IBinderPool mBinderPool; private synchronized void connectBinderPoolService() { Intent service = new Intent(); service.setComponent(new ComponentName("com.test.lz", "com.test.lz.BinderPoolService")); mContext.bindService(service, mBinderPoolConnection, Context.BIND_AUTO_CREATE); } public IBinder queryBinder(int binderCode) { IBinder binder = null; try { if (mBinderPool != null) { binder = mBinderPool.queryBinder(binderCode); } } catch (RemoteException e) { e.printStackTrace(); } return binder; } private ServiceConnection mBinderPoolConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mBinderPool = IBinderPool.Stub.asInterface(service); try { mBinderPool.asBinder().linkToDeath(mBinderPoolDeathRecipient, 0); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { } }; private IBinder.DeathRecipient mBinderPoolDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { Log.e(TAG, "binderDied"); mBinderPool.asBinder().unlinkToDeath(mBinderPoolDeathRecipient, 0); mBinderPool = null; connectBinderPoolService(); } };}
8. BroadcastReceiver
廣播,不用多說了吧~ 像我們可以監聽系統的開機廣播,網路變動廣播等等,都是體現了程序間通訊的作用。
總結這裡給大家提供一個方向,進行體系化的學習:
1、看影片進行系統學習
前幾年的Crud經歷,讓我明白自己真的算是菜雞中的戰鬥機,也正因為Crud,導致自己技術比較零散,也不夠深入不夠系統,所以重新進行學習是很有必要的。我差的是系統知識,差的結構框架和思路,所以透過影片來學習,效果更好,也更全面。關於影片學習,個人可以推薦去B站進行學習,B站上有很多學習影片,唯一的缺點就是免費的容易過時。
另外,我自己也珍藏了好幾套影片,有需要的我也可以分享給你。
2、進行系統梳理知識,提升儲備
客戶端開發的知識點就那麼多,面試問來問去還是那麼點東西。所以面試沒有其他的訣竅,只看你對這些知識點準備的充分程度。so,出去面試時先看看自己複習到了哪個階段就好。
系統學習方向:
架構師築基必備技能:深入Java泛型+註解深入淺出+併發程式設計+資料傳輸與序列化+Java虛擬機器原理+反射與類載入+動態代理+高效IOAndroid高階UI與FrameWork原始碼:高階UI晉升+Framework核心解析+Android元件核心+資料持久化360°全方面效能調優:設計思想與程式碼質量最佳化+程式效能最佳化+開發效率最佳化解讀開源框架設計思想:熱修復設計+外掛化框架解讀+元件化框架設計+圖片載入框架+網路訪問框架設計+RXJava響應式程式設計框架設計+IOC架構設計+Android架構元件JetpackNDK模組開發:NDK基礎知識體系+底層圖片處理+音影片開發微信小程式:小程式介紹+UI開發+API操作+微信對接Hybrid 開發與Flutter:Html5專案實戰+Flutter進階知識梳理完之後,就需要進行查漏補缺,所以針對這些知識點,我手頭上也準備了不少的電子書和筆記,這些筆記將各個知識點進行了完美的總結。
3、讀原始碼,看實戰筆記,學習大神思路
“程式語言是程式設計師的表達的方式,而架構是程式設計師對世界的認知”。所以,程式設計師要想快速認知並學習架構,讀原始碼是必不可少的。閱讀原始碼,是解決問題 + 理解事物,更重要的:看到原始碼背後的想法;程式設計師說:讀萬行原始碼,行萬種實踐。
4、面試前夕,刷題衝刺
面試的前一週時間內,就可以開始刷題衝刺了。請記住,刷題的時候,技術的優先,演算法的看些基本的,比如排序等即可,而智力題,除非是校招,否則一般不怎麼會問。
關於面試刷題,我個人也準備了一套系統的面試題,幫助你舉一反三:
還有耗時一年多整理的一系列Android學習資源:Android原始碼解析、Android第三方庫原始碼筆記、Android進階架構師七大專題學習、歷年BAT面試題解析包、Android大佬學習筆記等等。