與我以往的風(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 不是都有可能成立?
答:不可能。== 操作符比較的確實是對象地址沒錯,但是這里其實還隱含了兩個條件:
- 這個操作符比較的是 “那一刻” 兩個對象的地址。
- 比較的兩個對象都位于同一個進程內(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)限檢查),合理利用這個小玩意有奇效哦 :)