寫給 Android 開發(fā)者的系統(tǒng)基礎(chǔ)知識科普

與我以往的風(fēng)格不同,本文為科普類文章,因此不會涉及到太過高深難懂的知識。但這些內(nèi)容可能 Android 應(yīng)用層開發(fā)者甚至部分 framework 層開發(fā)者都不了解,因此仍舊高能預(yù)警。

另外廣東這兩天好冷啊,大家注意保暖~

虛擬機與運行時

對象的概念

假設(shè) getObjectAddress(Object) 是一個獲取對象內(nèi)存地址的方法。

第一題:

考慮如下代碼:

public static void main(String[] args) {
    Object o = new Object();
    long address1 = getObjectAddress(o);
    // .......
    long address2 = getObjectAddress(o);
}

main 方法中,創(chuàng)建了一個 Object 對象,隨后兩次調(diào)用 getObjectAddress 獲取該對象的地址。兩次獲取到的對象地址是否有可能不同?換句話說,對象的地址是否有可能變更?

答:有可能。JVM 中存在 GC 即“垃圾回收”機制,會回收不再使用的對象以騰出內(nèi)存空間。GC 可能會移動對象。

第二題:

考慮如下代碼:

private static long allocate() {
    Object o = new Object();
    return getObjectAddress(o);
}

public static void main(String[] args) {
    long address1 = allocate();
    // ......
    long address2 = allocate();
}

allocate() 創(chuàng)建了一個 Object 對象,然后獲取它的對象地址。 main 方法中調(diào)用兩次 allocate(),這兩個對象的內(nèi)存地址是否有可能相同?

答:有可能。在 allocate() 方法中創(chuàng)建的對象在該方法返回后便失去所有引用成為“不再需要的對象”,如果兩次方法調(diào)用之間,第一次方法調(diào)用中產(chǎn)生的臨時對象被上文中提到的 GC 機制回收,對應(yīng)的內(nèi)存空間就變得“空閑”,可以被其他對象占用。

第三題:

哎呀,既然上面說同一個對象的內(nèi)存地址可能不相同,兩個不同對象也有可能有相同的內(nèi)存地址,而java 里的 == 又是判斷對象的內(nèi)存地址,那么

Object o = new Object();
if (o != o)

還有

Object o1 = new Object();
Object o2 = new Object();
if (o1 == o2)

這里的兩個 if 不是都有可能成立?

答:不可能。== 操作符比較的確實是對象地址沒錯,但是這里其實還隱含了兩個條件:

  1. 這個操作符比較的是 “那一刻” 兩個對象的地址。
  2. 比較的兩個對象都位于同一個進程內(nèi)。

上述提到的兩種情況都不滿足“同一時間”這一條件,因此這兩條 if 永遠不會成立。

類與方法

第四題:

假設(shè) Framework 是 Android Framework 里的一個類,App 是某個 Android App 的一個類:

public class Framework {
    public static int api() {
        return 0;
    }
}

public class App {
    public static void main(String[] args) {
        Framework.api();
    }
}

編譯 App,然后將 Framework 內(nèi) api 方法的返回值類型從 int 改為 long,編譯 Framework 但不重新編譯 App,App 是否可以正常調(diào)用 Framework 的 api 方法?

答:不能。Java 類內(nèi)存儲的被調(diào)用方法的信息里包含返回值類型,如果返回值類型不對在運行時就找不到對應(yīng)方法。將方法改為成員變量然后修改該變量的類型也同理。

第五題:

考慮如下代碼:

class Parent {
    public void call() {
        privateMethod();
    }
    private void privateMethod() {
        System.out.println("Parent method called");
    }
}

class Child extends Parent {
    private void privateMethod() {
        System.out.println("Child method called");
    }
}

new Child().call();

Child 里的 privateMethod 是否重寫了 Parent 里的?call 中調(diào)用的 privateMethod() 會調(diào)用到 Parent 里的還是 Child 里的?

答:不構(gòu)成方法重寫,還是會調(diào)用到 Parent 里的 privateMethod。private 方法是 direct 方法,direct 方法無法被重寫。

操作系統(tǒng)基礎(chǔ)

多進程與虛擬內(nèi)存

假設(shè)有進程 A 和進程 B。

第六題:

進程 A 里的對象 a 和進程 B 里的對象 b 擁有相同的內(nèi)存地址,它們是同一個對象嗎?

答:當(dāng)然不是,上面才說過“對象相等”這個概念在同一個進程里才有意義,不認(rèn)真聽課思考是會被打屁屁的~

第七題:

進程 A 內(nèi)有一個對象 a 并將這個對象的內(nèi)存地址傳遞給了 B,B 是否可以直接訪問(讀取、寫入等操作)這個對象?

答:不能,大概率會觸發(fā)段錯誤,小概率會修改到自己內(nèi)存空間里某個冤種對象的數(shù)據(jù),無論如何都不會影響到進程 A。作為在用戶空間運行的進程,它們拿到的所謂內(nèi)存地址全部都是虛擬地址,進程訪問這些地址的時候會先經(jīng)過一個轉(zhuǎn)換過程轉(zhuǎn)化為物理地址再操作。如果轉(zhuǎn)換出錯(人家根本不認(rèn)識你給的這個地址,或者對應(yīng)內(nèi)存的權(quán)限不讓你執(zhí)行對應(yīng)操作),就會觸發(fā)段錯誤。

第八題:

還是我們可愛的進程 A 和 B,但是這次 B 是 A 的子進程,即 A 調(diào)用 fork 產(chǎn)生了 B 這個新的進程:

void a() {
    int* p = malloc(sizeof(int));
    *p = 1;
    if (fork() > 0) {
        // 進程 A 也即父進程
        // 巴拉巴拉巴拉一堆操作
    } else {
        // 進程 B 也即子進程
        *p = 2;
    }
}

(fork 是 Posix 內(nèi)創(chuàng)建進程的 API,調(diào)用完成后如果仍然在父進程則返回子進程的 pid 永遠大于 0,在子進程則返回 0)

(還是理解不了就把 A 想象為 Zygote 進程,B 想象為任意 App 進程)

這一段代碼分配了一段內(nèi)存,調(diào)用 fork 產(chǎn)生了一個子進程,然后在子進程里將預(yù)先分配好的那段內(nèi)存里的值更改為 2。 問:進程 B 做出的更改是否對進程 A 可見?

答:不可見,進程 A 看見的那一段內(nèi)存的值依然是 1。Linux 內(nèi)核有一個叫做“寫時復(fù)制”(Copy On Write)的技術(shù),在進程 B 嘗試寫入這一段內(nèi)存的時候會偷偷把真實的內(nèi)存給復(fù)制一份,最后寫入的是這份拷貝里的值,而進程 A 看見的還是原來的值。

跨進程大數(shù)據(jù)傳遞

已知進程 A 和進程 B,進程 A 暴露出一個 AIDL 接口,現(xiàn)在進程 B 要從 A 獲取 10M 的數(shù)據(jù)(遠遠超出 binder 數(shù)據(jù)大小限制),且禁止傳遞文件路徑,只允許調(diào)用這個 AIDL 接口一次,請問如何實現(xiàn)?

答:可以傳遞文件描述符(File Descriptor)。別以為這個玩意只能表示文件!舉個例子,作為應(yīng)用層開發(fā)者我們可以使用共享內(nèi)存的方法,這樣編寫 AIDL 實現(xiàn)類把數(shù)據(jù)傳遞出去:

@Override public SharedMemory getData() throws RemoteException {
    int size = 10 * 1024 * 1024;
    try {
        SharedMemory sharedMemory = SharedMemory.create("shared memory", size);
        ByteBuffer buffer = sharedMemory.mapReadWrite();
        for (int i = 0;i < 10;i++) {
            // 模擬產(chǎn)生一堆數(shù)據(jù)
            buffer.put(i * 1024 * 1024, (byte) 114);
            buffer.put(i * 1024 * 1024 + 1, (byte) 51);
            buffer.put(i * 1024 * 1024 + 2, (byte) 4);
            buffer.put(i * 1024 * 1024 + 3, (byte) 191);
            buffer.put(i * 1024 * 1024 + 4, (byte) 98);
            buffer.put(i * 1024 * 1024 + 5, (byte) 108);
            buffer.put(i * 1024 * 1024 + 6, (byte) 93);
        }
        SharedMemory.unmap(buffer);
        sharedMemory.setProtect(OsConstants.PROT_READ);
        return sharedMemory;
    } catch (ErrnoException e) {
        throw new RemoteException("remote create shared memory failed: " + e.getMessage());
    }
}

然后在進程 B 里這樣拿:

IRemoteService service = IRemoteService.Stub.asInterface(binder);
try {
    SharedMemory sharedMemory = service.getData();
    ByteBuffer buffer = sharedMemory.mapReadOnly();

    // 模擬處理數(shù)據(jù)
    int[] temp = new int[10];
    for (int i = 0;i < 10;i++) {
        for (int j = 0;j < 10;j++) {
            temp[j] = buffer.get(i * 1024 * 1024 + j);
        }
        Log.e(TAG, "Large buffer[" + i + "]=" + Arrays.toString(temp));
    }
    SharedMemory.unmap(buffer);
    sharedMemory.close();
} catch (Exception e) {
    throw new RuntimeException(e);
}

這里使用的 SharedMemory 從 Android 8.1 開始可用,在 8.1 之前的系統(tǒng)里也有一個叫做 MemoryFile 的 API 可以用。 打開 SharedMemory 里的源碼,你會發(fā)現(xiàn)其實它內(nèi)部就是創(chuàng)建了一塊 ashmem (匿名共享內(nèi)存),然后將對應(yīng)的文件描述符傳遞給 binder。內(nèi)核會負(fù)責(zé)將一個可用的文件描述符傳遞給目標(biāo)進程。 你可以將它理解為可以跨進程傳遞的 File Stream(只要能通過權(quán)限檢查),合理利用這個小玩意有奇效哦 :)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容