Android跨程序傳大圖思考及實現——附上原理分析
highlight: androidstudio theme: arknights
- 小知識,大挑戰!本文正在參與“程式設計師必備小知識”創作活動。
1.拋一個問題
這一天,法海想鍛鍊小青的定力,由於Bitmap也是一個Parcelable型別的資料,法海
想通過Intent
給小青
傳個特別大的圖片
kotlin
intent.putExtra("myBitmap",fhBitmap)
如果“法海”
(Activity)使用Intent去傳遞一個大的Bitmap
給“小青”
(Activity),如果你的圖片夠大,會出現類似下面這樣的錯誤,請繼續往下看:
text
Caused by: android.os.TransactionTooLargeException: data parcel size 8294952 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:535)
at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3904)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1738)
至於是什麼樣的大圖,這個只有法海知道了(小青:好羞澀啊)
🙈🙈🙈
所以TransactionTooLargeException這玩意爆出來的地方在哪呢?
2.問題定位分析
我們可以看到錯誤的日誌資訊裡面看到呼叫了BinderProxy.transactNative
,這個transactNative是一個native方法
java
//android.os.BinderProxy
/**
* Native implementation of transact() for proxies
*/
public native boolean transactNative(int code, Parcel data, Parcel reply,
int flags) throws RemoteException;
在Android Code Search,全域性搜尋一下:android_os_BinderProxy_transact
```c++
//frameworks/base/core/jni/android_util_Binder.cpp
static jboolean android_os_BinderProxy_transact(JNIEnv env, jobject obj, jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException { ...... status_t err = target->transact(code, data, reply, flags); ...... if (err == NO_ERROR) { //如果匹配成功直接攔截不往下面執行了 return JNI_TRUE; } else if (err == UNKNOWN_TRANSACTION) { return JNI_FALSE; } signalExceptionForError(env, obj, err, true /canThrowRemoteException/, data->dataSize()); return JNI_FALSE; } ``` 我們開啟signalExceptionForError方法看看裡面的內容
```c++ //frameworks/base/core/jni/android_util_Binder.cpp //處理異常的方法 void signalExceptionForError(JNIEnv env, jobject obj, status_t err, bool canThrowRemoteException, int parcelSize) { switch (err) { //其他異常,大家可以自行閱讀了解; //如:沒有許可權異常,檔案太大,錯誤的檔案描述符,等等; ........ case FAILED_TRANSACTION: { const char exceptionToThrow; char msg[128]; //官方在FIXME中寫道:事務過大是FAILED_TRANSACTION最常見的原因 //但它不是唯一的原因,Binder驅動可以返回 BR_FAILED_REPLY //也有其他原因可能是:事務格式不正確,檔案描述符FD已經被關閉等等
//parcelSize大於200K就會報錯,canThrowRemoteException傳遞進來的是true
if (canThrowRemoteException && parcelSize > 200*1024) {
// bona fide large payload
exceptionToThrow = "android/os/TransactionTooLargeException";
snprintf(msg, sizeof(msg)-1, "data parcel size %d bytes", parcelSize);
} else {
..........
}
//使用指定的類和訊息內容丟擲異常
jniThrowException(env, exceptionToThrow, msg);
} break;
........
}
} ``` 此時我們看到: parcelSize大於200K就會報錯,難道一定是200K以內?先彆著急著下結論,繼續往下看👇👇
3.提出疑問
法海:我有個疑問,我看到文件寫的1M大小啊;
許仙:別急,妹夫,來先看一下文件的解釋,看一下使用說明:
官方TransactionTooLargeException的文件中描述到:Binder 事務緩衝區有一個有限的固定大小,目前為 1MB,由程序所有正在進行的事務共享
可以看到寫的是:共享事務的緩衝區
如來佛祖:汝等別急,我們簡單測試一下,Intent傳遞201*1024
個位元組陣列,我們發現可以正常傳遞過去,Logcat僅僅輸出了一個Error提示的日誌資訊,還是可以正常傳遞的
text
E/ActivityTaskManager: Transaction too large, intent: Intent { cmp=com.melody.test/.SecondActivity (has extras) }, extras size: 205848, icicle size: 0
我們再測試一個值,intent傳遞800*1024
個位元組陣列,我們發現會崩潰
text
android.os.TransactionTooLargeException: data parcel size 821976 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:540)
at android.app.IApplicationThread$Stub$Proxy.scheduleTransaction(IApplicationThread.java:2504)
at android.app.servertransaction.ClientTransaction.schedule(ClientTransaction.java:136)
不要著急,我們繼續往下看分析
4.解答疑問
我們來看一下,下面兩行程式碼
c++
//frameworks/base/core/jni/android_util_Binder.cpp
//這個方法android_os_BinderProxy_transact裡面的
IBinder* target = getBPNativeData(env, obj)->mObject.get();
status_t err = target->transact(code, *data, reply, flags);
從上面的分析和測試結果,我們從target->transact
這裡來找err返回值
,
先根據標頭檔案,搜尋對應的cpp類,我們看一下這幾個cpp類:BpBinder.cpp、
IPCThreadState.cpp、ProcessState.cpp
```c++
//frameworks/native/libs/binder/ProcessState.cpp
// (1 * 1024 * 1024) - (4096 *2)
define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)
define DEFAULT_MAX_BINDER_THREADS 15
//下面兩個註釋 //引用自官方文件:https://source.android.google.cn/devices/architecture/hidl/binder-ipc
ifdef ANDROID_VNDK
//供應商/供應商程序之間的IPC,使用 AIDL 介面 const char* kDefaultDriver = "/dev/vndbinder";
else
// "/dev/binder" 裝置節點成為框架程序的專有節點 const char* kDefaultDriver = "/dev/binder";
endif
//建構函式:初始化一些變數,Binder最大執行緒數等
ProcessState::ProcessState(const char driver)
: mDriverName(String8(driver)),
mDriverFD(-1),
mVMStart(MAP_FAILED),
......
mMaxThreads(DEFAULT_MAX_BINDER_THREADS),
mStarvationStartTimeMs(0),
mThreadPoolStarted(false),
mThreadPoolSeq(1),
mCallRestriction(CallRestriction::NONE) {
......
//開啟驅動
base::Result
這裡為什麼不是1M,而是1M-8K?
最開始的時候,官方寫的是1M,後來他們內部自己優化了;
來看這裡👉👉官方提交的ProcessState.cpp提交的log日誌:允許核心更有效地利用其虛擬地址空間
我們知道:微信的MMKV
、美團的Logan的日誌元件
,都是基於mmap
來實現的;
binder驅動的註冊邏輯在Binder.c中,我們看一下binder_mmap
方法
c++
//kernel/msm/drivers/android/binder.c
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
int ret;
struct binder_proc *proc = filp->private_data;
const char *failure_string;
if (proc->tsk != current->group_leader)
return -EINVAL;
//這裡可以看到:對映空間最多4M
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;
......
//初始化指定的空間vma用於分配繫結緩衝區
ret = binder_alloc_mmap_handler(&proc->alloc, vma);
......
}
這裡能看到對映空間最多4M,我們再來看一下binder_alloc_mmap_handler這個方法,點選檢視binder_alloc.c
c++
//kernel/msm/drivers/android/binder_alloc.c
//由binder_mmap()呼叫來初始化指定的空間vma用於分配繫結緩衝區
int binder_alloc_mmap_handler(struct binder_alloc *alloc,
struct vm_area_struct *vma)
{
......
//buffer_size最大4M
alloc->buffer_size = vma->vm_end - vma->vm_start;
......
//非同步事務的空閒緩衝區大小最大2M
alloc->free_async_space = alloc->buffer_size / 2;
......
}
從上面的分析得出結論:
1.Binder驅動給每個程序最多分配4M的buffer空間大小;
2.非同步事務的空閒緩衝區空間大小最多為2M;
3.Binder核心記憶體上限為1M-8k;
4.非同步事務緩衝區空間大小等於buffer_size/2,具體值取決於buffer_size;
同步、非同步是定義在AIDL檔案中的,我們看上面測試的兩個例子,其中有一個傳了800*1024
個位元組陣列崩潰如下:
text
android.os.TransactionTooLargeException: data parcel size 821976 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:540)
at android.app.IApplicationThread$Stub$Proxy.scheduleTransaction(IApplicationThread.java:2504)
點選檢視IApplicationThread.aidl
檢視AIDL裡面的內容,我們看到scheduleTransaction是一個非同步的方法;
因為oneway修飾在interface之前,會讓interface內所有的方法都隱式地帶上oneway;
由於oneway非同步呼叫,我們這個時候修改一下,傳遞(1M-8k)/2
大小之內的資料測試一下
```text
// ((1024 * 1024 - 8 * 1024)/2)-1
E/ActivityTaskManager: Transaction too large, intent: Intent { cmp=com.melody.test/.SecondActivity (has extras) }, extras size: 520236, icicle size: 0
Exception when starting activity com.melody.test/.SecondActivity
android.os.TransactionTooLargeException: data parcel size 522968 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:540)
at android.app.IApplicationThread$Stub$Proxy.scheduleTransaction(IApplicationThread.java:2504)
``
可以看到還是會報錯,說明非同步事務的可用空間不夠,仔細看一下為什麼不夠,細心的同學可能發現了:
**警告的日誌列印**:
extras size: 520236**崩潰的日誌列印**:
data parcel size: 522968**大小相差**:
2732約等於
2.7k`
如果這個時候我們用Intent傳遞一個ByteArray,比之前的大小減去3k
,
ByteArray((1024*1024 - (8 * 1024))/2 - 3 * 1024)
kotlin
startActivity(Intent(this,SecondActivity::class.java).apply {
putExtra("KEY",ByteArray((1024*1024 - (8 * 1024))/2 - 3 * 1024))
})
這個時候發現(1M-8k)/2 -3k
,可以成功傳遞資料,說明有其他資料佔用了這部分空間。
我們上面寫了,不要忘記:共享事務的緩衝區,這裡減去3k僅測試用的
,我們繼續往下分析;
找一下:非同步事務的空閒緩衝區空間大小比較的地方,開啟binder_alloc.c,找到binder_alloc_new_buf
方法
c++
//kernel/msm/drivers/android/binder_alloc.c
//分配一個新緩衝區
struct binder_buffer *binder_alloc_new_buf(struct binder_alloc *alloc,
size_t data_size,
size_t offsets_size,
size_t extra_buffers_size,
int is_async,
int pid)
{
......
buffer = binder_alloc_new_buf_locked(alloc, data_size, offsets_size,extra_buffers_size, is_async, pid);
.......
}
我們來看一下binder_alloc_new_buf_locked方法
c++
//kernel/msm/drivers/android/binder_alloc.c
static struct binder_buffer *binder_alloc_new_buf_locked(
struct binder_alloc *alloc,
size_t data_size,
size_t offsets_size,
size_t extra_buffers_size,
int is_async,
int pid)
{
......
//如果是非同步事務,檢查所需的大小是否在非同步事務的空閒緩衝區區間內
if (is_async &&
alloc->free_async_space < size + sizeof(struct binder_buffer)) {
return ERR_PTR(-ENOSPC);
}
}
分析了這麼多,不論是同步還是非同步,都是共享事務的緩衝區,如果有大量資料需要通過Activity的Intent傳遞,資料大小最好維持在200k以內;
上面測試的時候,超出200k資料傳遞的時候,LogCat已經給我們列印提示“Transaction too large”
了,但是只要
沒有超出非同步事務空閒的緩衝區大小,就不會崩潰
;
如果Intent傳遞大量的資料完全可以使用別的方式方法;
5.Intent設定Bitmap發生了什麼?
5.1-Intent.writeToParcel
Intent資料寫入到parcel中,在writeToParcel方法裡面,Intent把Bundle寫入到Parcel中
```java //android.content.Intent
public void writeToParcel(Parcel out, int flags) {
......
//把Bundle寫入到Parcel中
out.writeBundle(mExtras);
}
``
開啟
out.writeBundle`方法
java
//android.os.Parcel#writeBundle
public final void writeBundle(@Nullable Bundle val) {
if (val == null) {
writeInt(-1);
return;
}
//執行Bundle自身的writeToParcel方法
val.writeToParcel(this, 0);
}
5.2-Bundle.writeToParcel
```java //android.os.Bundle
public void writeToParcel(Parcel parcel, int flags) {
final boolean oldAllowFds = parcel.pushAllowFds((mFlags & FLAG_ALLOW_FDS) != 0);
try {
//這裡官方註釋已經寫的很詳細了:
//將Bundle內容寫入Parcel,通常是為了讓它通過IBinder連線傳遞
super.writeToParcelInner(parcel, flags);
} finally {
//把mAllowFds值設定回來
parcel.restoreAllowFds(oldAllowFds);
}
}
``
[**點選檢視Parcel.cpp**](https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/Parcel.cpp),我們看一下里面的
pushAllowFds`方法
c++
//frameworks/native/libs/binder/Parcel.cpp
bool Parcel::pushAllowFds(bool allowFds)
{
const bool origValue = mAllowFds;
if (!allowFds) {
mAllowFds = false;
}
return origValue;
}
如果Bundle設定了不允許帶描述符,當呼叫pushAllowFds之後Parcel中的內容也不帶描述符;
在文章開頭,我們舉的例子中:通過Intent去傳遞一個Bitmap,在執行到Instrumentation#execStartActivity
的時候,我們發現Intent有個prepareToLeaveProcess
方法,在此方法裡面呼叫了Bundle#setAllowFds(false)
java
//android.app.Instrumentation
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
try {
......
intent.prepareToLeaveProcess(who);
......
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
5.3-Parcel.writeArrayMapInternal
剛剛上面Bundle.writeToParcel方法裡面super.writeToParcelInner
觸發下面方法
java
//android.os.BaseBundle
void writeToParcelInner(Parcel parcel, int flags) {
......
parcel.writeArrayMapInternal(map);
......
}
我們看一下writeArrayMapInternal
方法
java
void writeArrayMapInternal(@Nullable ArrayMap<String, Object> val) {
......
for (int i=0; i<N; i++) {
writeString(val.keyAt(i));
//根據不同資料型別呼叫不同的write方法
writeValue(val.valueAt(i));
}
}
5.4-writeValue
文章一開頭我們使用的是intent.putExtra("bmp",法海bitmap)
java
//android.os.Parcel
public final void writeValue(@Nullable Object v) {
......
if (v instanceof Parcelable) {
writeInt(VAL_PARCELABLE);
writeParcelable((Parcelable) v, 0);
}
......
}
public final void writeParcelable(@Nullable Parcelable p, int parcelableFlags) {
......
writeParcelableCreator(p);
p.writeToParcel(this, parcelableFlags);
}
因為傳入的是Bitmap,我們看Bitmap.writeToParcel
5.5-Bitmap.writeToParcel
java
//android.graphics.Bitmap
public void writeToParcel(Parcel p, int flags) {
noteHardwareBitmapSlowCall();
//開啟Bitmap.cpp找對應的native方法
if (!nativeWriteToParcel(mNativePtr, mDensity, p)) {
throw new RuntimeException("native writeToParcel failed");
}
}
點選開啟Bitmap.cpp,檢視Bitmap_writeToParcel
方法
```c++ //frameworks/base/libs/hwui/jni/Bitmap.cpp
static jboolean Bitmap_writeToParcel(JNIEnv env, jobject,
jlong bitmapHandle, jint density, jobject parcel) {
......
//獲得Native層的物件
android::Parcel p = parcelForJavaObject(env, parcel);
SkBitmap bitmap;
auto bitmapWrapper = reinterpret_cast
// Transfer the underlying ashmem region if we have one and it's immutable.
android::status_t status;
int fd = bitmapWrapper->bitmap().getAshmemFd();
if (fd >= 0 && bitmap.isImmutable() && p->allowFds()) {
//AshmemFd大於等於0 && bitmap不可變 && parcel允許帶Fd
//符合上述條件,將fd寫入到parcel中
status = p->writeDupImmutableBlobFileDescriptor(fd);
if (status) {
doThrowRE(env, "Could not write bitmap blob file descriptor.");
return JNI_FALSE;
}
return JNI_TRUE;
}
//mutableCopy=true:表示bitmap是可變的
const bool mutableCopy = !bitmap.isImmutable();
//返回畫素儲存所需的最小記憶體
size_t size = bitmap.computeByteSize();
android::Parcel::WritableBlob blob;
//獲取到一塊blob緩衝區,往下翻有程式碼分析
status = p->writeBlob(size, mutableCopy, &blob);
......
} ``` 我們來看看writeBlob裡面做了什麼事情
5.6-Parcel::writeBlob
```java //frameworks/native/libs/binder/Parcel.cpp
static const size_t BLOB_INPLACE_LIMIT = 16 * 1024; // 16k
status_t Parcel::writeBlob(size_t len, bool mutableCopy, WritableBlob outBlob)
{
status_t status;
if (!mAllowFds || len <= BLOB_INPLACE_LIMIT) {
//如果不允許帶FD 或者 資料小於等於16k,則直接將圖片寫入到parcel中
status = writeInt32(BLOB_INPLACE);
if (status) return status;
void ptr = writeInplace(len);
if (!ptr) return NO_MEMORY;
outBlob->init(-1, ptr, len, false);
return NO_ERROR;
}
//不滿足上面的條件,即(允許Fd && len > 16k):
//建立一個新的ashmem區域並返回檔案描述符FD
//ashmem-dev.cpp裡面有註釋說明:
//https://cs.android.com/android/platform/superproject/+/master:system/core/libcutils/ashmem-dev.cpp
int fd = ashmem_create_region("Parcel Blob", len);
if (fd < 0) return NO_MEMORY;
//設定ashmem這塊區域是“可讀可寫”
int result = ashmem_set_prot_region(fd, PROT_READ | PROT_WRITE);
if (result < 0) {
status = result;
} else {
//根據fd,對映 “len大小” 的mmap的空間
void ptr = ::mmap(nullptr, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
......
if (!status) {
//將fd寫入到parcel中
status = writeFileDescriptor(fd, true /takeOwnership*/);
if (!status) {
outBlob->init(fd, ptr, len, mutableCopy);
return NO_ERROR;
}
}
......
}
......
}
``
看到這裡,大家應該知道我們為什麼先分析Intent傳遞資料大小的上限了吧;
在[**目錄5**](#heading-4)下面的
[**5.2-Bundle.writeToParcel**](#heading-6)已經說明清楚了,Intent啟動Activity的時候,禁用掉了檔案描述符;
**所以:** 在執行writeBlob方法只能執行到第一個分支,
直接將圖片寫入到parcel中`,我們在目錄4給出Intent傳遞資料大小限制的結論;
那麼如何不受Intent禁用檔案描述符和資料大小的限制?
6.跨程序傳大圖
在Parcel類中看到writeValue方法裡面有個分支,判斷當前value是不是IBinder,如果是IBinder型別的會呼叫writeStrongBinder把這個物件寫入到Parcel中;
所以我們可以使用Bundle的putBinder來把IBinder物件
寫入到Parcel中,通過putBinder
不會受Intent禁用檔案描述符的影響,資料大小也沒有限制,Bitmap寫入到parcel中預設是true,可以使用匿名共享記憶體(Ashmem);
6.1-單程序下putBinder用法
```kotlin //定義一個IntentBinder,此方法僅在『同一個程序』下有效哦,切記切記!!!! class IntentBinder(val imageBmp:Bitmap? = null): Binder()
//------------------------使用如下--------------------------// //com.xxx.xxx.MainActivity val bitmap = BitmapFactory.decodeStream(...) startActivity(Intent(this,SecondActivity::class.java).putExtras(Bundle().apply { putBinder("myBinder",IntentBinder(bitmap)) }))
//------------------------獲取Bitmap並顯示如下--------------------------//
//com.xxx.xxx.SecondActivity
val bundle: Bundle? = intent.extras
val imageBinder:IntentBinder? = bundle?.getBinder("myBinder") as IntentBinder?
//拿到Binder中的Bitmap
val bitmap = imageBinder?.imageBmp
//自行壓縮後顯示到ImageView上.....
**注意:** 這個用法`不能`跨程序,喜歡動手的同學,可以試一試,給SecondActivity配置一個`android:process=":remote"`,你會發現會報一個`強制轉換的異常錯誤`
text
//錯誤的用在多程序場景下,報錯如下:
java.lang.ClassCastException: android.os.BinderProxy cannot be cast to com.xxx.xxx.IntentBinder
```
🤔為什麼可以通過這種方式傳遞物件?
Binder會為我們的物件建立一個全域性的JNI引用,點選檢視android_util_Binder.cpp
```c++ //frameworks/base/core/jni/android_util_Binder.cpp ...... static struct bindernative_offsets_t { // Class state. jclass mClass; jmethodID mExecTransact; jmethodID mGetInterfaceDescriptor;
// Object state.
jfieldID mObject;
} gBinderOffsets; ...... static const JNINativeMethod gBinderMethods[] = { / name, signature, funcPtr / // @CriticalNative { "getCallingPid", "()I", (void)android_os_Binder_getCallingPid }, // @CriticalNative { "getCallingUid", "()I", (void)android_os_Binder_getCallingUid }, ...... { "getExtension", "()Landroid/os/IBinder;", (void)android_os_Binder_getExtension }, { "setExtension", "(Landroid/os/IBinder;)V", (void)android_os_Binder_setExtension }, };
const char* const kBinderPathName = "android/os/Binder";
//呼叫下面這個方法,完成Binder類的註冊 static int int_register_android_os_Binder(JNIEnv* env) { //獲取Binder的class物件 jclass clazz = FindClassOrDie(env, kBinderPathName);
//內部建立全域性引用,並將clazz儲存到全域性變數中
gBinderOffsets.mClass = MakeGlobalRefOrDie(env, clazz);
//獲取Java層的Binder的成員方法execTransact
gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact", "(IJJI)Z");
//獲取Java層的Binder的成員方法getInterfaceDescriptor
gBinderOffsets.mGetInterfaceDescriptor = GetMethodIDOrDie(env, clazz, "getInterfaceDescriptor",
"()Ljava/lang/String;");
//獲取Java層的Binder的成員變數mObject
gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J");
//註冊gBinderMethods中定義的函式
return RegisterMethodsOrDie(
env, kBinderPathName,
gBinderMethods, NELEM(gBinderMethods));
} ...... ```
6.2-多程序下putBinder用法
```kotlin //先定義一個IGetBitmapService.aidl package com.xxx.aidl; interface IGetBitmapService { Bitmap getIntentBitmap(); }
//------------------------使用如下--------------------------// //com.xxx.xxx.MainActivity 👉程序A val bitmap = BitmapFactory.decodeStream(...) startActivity(Intent(this,SecondActivity::class.java).putExtras(Bundle().apply { putBinder("myBinder",object: IGetBitmapService.Stub() { override fun getIntentBitmap(): Bitmap { return bitmap } }) }))
//------------------------獲取Bitmap並顯示如下--------------------------// //com.xxx.xxx.SecondActivity 👉程序B val bundle: Bundle? = intent.extras //返回IGetBitmapService型別 val getBitmapService = IGetBitmapService.Stub.asInterface(bundle?.getBinder("myBinder")) val bitmap = getBitmapService.intentBitmap //自行壓縮後顯示到ImageView上..... ```
7.參考
1.Binder kernel 基礎方法
2.Android Binder 魅族核心團隊
3.Android系統匿名共享記憶體Ashmem驅動程式原始碼分析
4.Android 匿名共享記憶體的使用
往期文章推薦:
1.Jetpack Compose UI建立佈局繪製流程+原理 —— 內含概念詳解(滿滿乾貨)
2.Jetpack App Startup如何使用及原理分析
3.Jetpack Compose - Accompanist 元件庫
4.原始碼分析 | ThreadedRenderer空指標問題,順便把Choreographer認識一下
5.原始碼分析 | 事件是怎麼傳遞到Activity的?
6.聊聊CountDownLatch 原始碼
7.Android正確的保活方案,不要掉進保活需求死迴圈陷進
- 鴻蒙ArkUI如何開發跨平臺應用?
- HarmonyOS玩轉ArkUI動效 - 水母動畫
- Compose挑燈夜看 - 照亮手機螢幕裡面的書本內容
- 順手修復了Jetpack Compose官方文件中的一個多點觸控示例的Bug
- 正確實踐Jetpack SplashScreen API —— 在所有Android系統上使用總結,內含原理分析
- Jetpack Compose處理“導航欄、狀態列、鍵盤” 影響內容顯示的問題集錦
- 閒聊Android懸浮的“系統文字選擇選單”和“ActionMode解析”——附上原理分析
- Jetpack Compose實現bringToFront功能——附上原理分析
- Android跨程序傳大圖思考及實現——附上原理分析