首頁>技術>

前言

今天看到一道騰訊面試題,關於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大佬學習筆記等等。

10
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • C++那些事:C++引入的 4 種類型轉換,應該怎樣使用?