Android最強保活黑科技的最強技術實現!
BATcoder技術 群,讓一部分人先進大廠
大家 好 ,我是劉望舒,騰訊TVP,著有三本業內知名暢銷書,連續四年蟬聯電子工業出版社年度優秀作者,百度百科收錄的資深技術專家。
前華為架構師,現大廠技術總監。
想要
加入
BATcoder技術群,公號回覆
BAT
即可。
作者:小頑童
https://juejin.cn/post/6844904110219608078
大家好,我是老玩童。今天來跟大家分享TIM最強保活思路的幾種實現方法。這篇文章我將通過ioctl跟binder驅動互動,實現以最快的方式喚醒新的保活服務,最大程度防止保活失敗。同時,我也將跟您分享,我是怎麼做到在不甚瞭解binder的情況下,快速實現ioctl binder這種高階操作。
宣告:現在這個保活方式在MIUI等定製Android系統中已經不能保活,大部分時候 只能活在模擬器中 了。但對與我們的輕量定製的Android系統,一些系統級應用的保活,這個方案還是有用的。
隨著Android陣營的各大手機廠商對於續航的高度重視,兩三年前的手機發佈會更是把反保活作為一個系統的賣點,不斷提出了各種反保活的方案,導致現在想實現應用保活簡直難於上青天,甚至都需要一個團隊來專門研究這個事情。連微信這種超級APP,也要拜倒在反保活的石榴裙下,允許後臺啟動太費電,不允許後臺啟動就收不到訊息。。Android發現了一個保活野路子就堵一條,然而很多場景是有保活的強需求的,有木有考慮過我們開發者的感受,自己人何必為難自己人:sob:。
我覺得這是一個Android設計的不合理的地方,路子可以堵,但還是有必要留一個統一的保活介面的。這個介面由Google實現也好,廠商來實現也好,總好過現在很笨拙的系統自啟動管理或者是JobScheduler。我覺得本質上來說,讓應用開發者想盡各種辦法去做保活,這個事情是沒有意義的,保活的路子被封了,但保活還是需要做,保活的成本也提高了,簡直浪費生命。 Android的鍋 。(僅代表個人觀點)
黑科技程序保活原理
大概2個月前,Gityuan大佬放出了一份分析TIM的黑科技保活的部落格 史上最強Android保活思路:深入剖析騰訊TIM的程序永生技術 [1] (後來不知道什麼原因又刪除了),頓時間掀起了一陣波瀾,彷彿讓開發者們又看到了應用保活的一絲希望。Gityuan大佬通過超強的專業技術分析,為我們解開了TIM保活方案的終極奧義。
後來,為數不多的維術大佬在Gityuan大佬的基礎上,釋出了部落格 Android 黑科技保活實現原理揭祕 [2] 又進行了系統程序查殺相關的原始碼分析。為我們帶來的結論是,Android系統殺應用的時候,會去殺程序組, 迴圈 40 遍不停地殺程序,每次殺完之後等 5ms 。
總之,引用維術的話語,原理如下:
-
利用Linux檔案鎖的原理,使用2個程序互相監聽各自的檔案鎖,來感知彼此的死亡。
-
通過 fork 產生子程序,fork 的程序同屬一個程序組,一個被殺之後會觸發另外一個程序被殺,從而被檔案鎖感知。
具體來說,建立 2 個程序 p1, p2,這兩個程序通過檔案鎖互相關聯,一個被殺之後拉起另外一個;同時 p1 經過 2 次 fork 產生孤兒程序 c1,p2 經過 2 次 fork 產生孤兒程序 c2,c1 和 c2 之間建立檔案鎖關聯。這樣假設 p1 被殺,那麼 p2 會立馬感知到,然後 p1 和 c1 同屬一個程序組,p1 被殺會觸發 c1 被殺,c1 死後 c2 立馬感受到從而拉起 p1,因此這四個程序三三之間形成了鐵三角,從而保證了存活率。
按照維術大佬的理論, 只要程序我復活的足夠快,系統它就殺不死我 ,嘿嘿。
維術大佬寫了一個簡單的實現,程式碼在這裡: github.com/tiann/Leori… [3] ,這個方案是當檢測到程序被殺時,會通過JNI的方式,呼叫Java層的方法來複活程序。為了實現穩定的保活,尤其是系統殺程序只給了5ms復活的機會,使用JNI這種方式復活程序現在達不到最優的效果。
Java 層復活程序
復活程序,其實就是啟動指定的Service。當native層檢測到有程序被殺時,為了能夠快速啟動新Service。我們可以通過反射,拿到ActivityManager的remote binder,直接通過這個binder傳送資料,即可實現快速啟動Service。
Class<?> amnCls = Class.forName("android.app.ActivityManagerNative");
amn = activityManagerNative.getMethod("getDefault").invoke(amnCls);
Field mRemoteField = amn.getClass().getDeclaredField("mRemote");
mRemoteField.setAccessible(true);
mRemote = (IBinder) mRemoteField.get(amn);
啟動Service的Intent:
Intent intent = new Intent();
ComponentName component = new ComponentName(context.getPackageName(), serviceName);
intent.setComponent(component);
封裝啟動Service的Parcel:
Parcel mServiceData = Parcel.obtain();
mServiceData.writeInterfaceToken("android.app.IActivityManager");
mServiceData.writeStrongBinder(null);
mServiceData.writeInt(1);
intent.writeToParcel(mServiceData, 0);
mServiceData.writeString(null); // resolvedType
mServiceData.writeInt(0);
mServiceData.writeString(context.getPackageName()); // callingPackage
mServiceData.writeInt(0);
啟動Service:
mRemote.transact(transactCode, mServiceData, null, 1);
在 native 層進行 binder 通訊
在Java層做程序復活的工作,這個方式是比較低效的,最好的方式是在 native 層使用純 C/C++來複活程序。方案有兩個。
其一,維術大佬給出的方案是利用libbinder.so, 利用Android提供的C++介面,跟ActivityManagerService通訊,以喚醒新程序。
-
Java 層建立 Parcel (含 Intent),拿到 Parcel 物件的 mNativePtr(native peer),傳到 Native 層。
-
native 層直接把 mNativePtr 強轉為結構體指標。
-
fork 子程序,建立管道,準備傳輸 parcel 資料。
-
子程序讀管道,拿到二進位制流,重組為 parcel。
其二,Gityuan大佬則認為使用 ioctl 直接給 binder 驅動傳送資料以喚醒程序,才是更高效的做法。然而,這個方法,大佬們並沒有提供思路。
那麼今天,我們就來實現這兩種在 native 層進行 Binder 呼叫的騷操作。
方式一 利用 libbinder.so 與 ActivityManagerService 通訊
上面在Java層復活程序一節中,是向ActivityManagerService傳送特定的封裝了Intent的Parcel包來實現喚醒程序。而在native層,沒有Intent這個類。所以就需要在Java層建立好Intent,然後寫到Parcel裡,再傳到Native層。
Parcel mServiceData = Parcel.obtain();
mServiceData.writeInterfaceToken("android.app.IActivityManager");
mServiceData.writeStrongBinder(null);
mServiceData.writeInt(1);
intent.writeToParcel(mServiceData, 0);
mServiceData.writeString(null); // resolvedType
mServiceData.writeInt(0);
mServiceData.writeString(context.getPackageName()); // callingPackage
mServiceData.writeInt(0);
檢視 Parcel的原始碼 [4] 可以看到,Parcel類有一個mNativePtr變數:
private long mNativePtr; // used by native code
// android4.4 mNativePtr是int型別
可以通過反射得到這個變數:
private static long getNativePtr(Parcel parcel) {
try {
Field ptrField = parcel.getClass().getDeclaredField("mNativePtr");
ptrField.setAccessible(true);
return (long) ptrField.get(parcel);
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
這個變數對應了C++中 Parcel類 [5] 的地址,因此可以強轉得到Parcel指標:
Parcel *parcel = (Parcel *) parcel_ptr;
然而,NDK中並沒有提供binder這個模組,我們只能從Android原始碼中扒到binder相關的原始碼,再編譯出libbinder.so。騰訊TIM應該就是魔改了binder相關的原始碼。
提取libbinder.so
為了避免libbinder的版本相容問題,這裡我們可以採用一個更簡單的方式,拿到binder相關的標頭檔案,再從系統中拿到libbinder.so,當然binder模組還依賴了其它的幾個so,要一起拿到,不然編譯的時候會報連結錯誤。
adb pull /system/lib/libbinder.so ./
adb pull /system/lib/libcutils.so ./
adb pull /system/lib/libc.so ./
adb pull /system/lib/libutils.so ./
如果需要不同SDK版本,不同架構的系統so庫,可以在 Google Factory Images [6] 網頁裡找到適合的版本,下載相應的韌體,然後解包system.img(需要在windows或linux中操作),提取出目標so。
binder_libs
├── arm64-v8a
│ ├── libbinder.so
│ ├── libc.so
│ ├── libcutils.so
│ └── libutils.so
├── armeabi-v7a
│ ├── ...
├── x86
│ ├── ...
└── x86_64
├── ...
為了避免相容問題,我這裡只讓這些so參與了binder相關的標頭檔案的連結,而沒有實際使用這些so。這是利用了so的載入機制,如果應用lib目錄沒有相應的so,則會到system/lib目錄下查詢。
SDK24以上,系統禁止了從system中載入so的方式,所以使用這個方法務必保證targetApi <24。
否則,將會報找不到so的錯誤。可以把上面的so放到jniLibs目錄解決這個問題,但這樣就會有相容問題了。
CMake修改:
# 連結binder_libs目錄下的所有so庫
link_directories(binder_libs/${CMAKE_ANDROID_ARCH_ABI})
# 引入binder相關的標頭檔案
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include/)
# libbinder.so libcutils.so libutils.so libc.so等庫連結到libkeep_alive.so
target_link_libraries(
keep_alive
${log-lib} binder cutils utils c)
程序間傳輸Parcel物件
C++裡面還能傳輸物件?不存在的。好在Parcel能直接拿到資料地址,並提供了構造方法。所以我們可以通過管道把Parcel資料傳輸到其它程序。
Parcel *parcel = (Parcel *) parcel_ptr;
size_t data_size = parcel->dataSize();
int fd[2];
// 建立管道
if (pipe(fd) < 0) {return;}
pid_t pid;
// 建立子程序
if ((pid = fork()) < 0) {
exit(-1);
} else if (pid == 0) {//第一個子程序
if ((pid = fork()) < 0) {
exit(-1);
} else if (pid > 0) {
// 託孤
exit(0);
}
uint8_t data[data_size];
// 託孤的子程序,讀取管道中的資料
int result = read(fd[0], data, data_size);
}
// 父程序向管道中寫資料
int result = write(fd[1], parcel->data(), data_size);
重新建立Parcel:
Parcel parcel;
parcel.setData(data, data_size);
傳輸Parcel資料
// 獲取ServiceManager
sp<IServiceManager> sm = defaultServiceManager();
// 獲取ActivityManager binder
sp<IBinder> binder = sm->getService(String16("activity"));
// 傳輸parcel
int result = binder.get()->transact(code, parcel, NULL, 0);
方式二 使用 ioctl 與 binder 驅動通訊
方式一讓我嚐到了一點甜頭,實現了大佬的思路,不禁讓鄙人浮想聯翩,感慨萬千,鄙人的造詣已經如此之深,不久就會人在美國,剛下飛機,迎娶白富美,走向人生巔峰矣......

咳咳。不禁想到ioctl的方式我也可以嘗試著實現一下。ioctl是一個linux標準方法,那麼我們就直奔主題看看,binder是什麼,ioctl怎麼跟binder driver通訊。
Binder介紹
Binder是Android系統提供的一種IPC機制。每個Android的程序,都可以有一塊使用者空間和核心空間。使用者空間在不同程序間不能共享,核心空間可以共享。Binder就是一個利用可以共享的核心空間,完成高效能的程序間通訊的方案。
Binder通訊採用C/S架構,從元件視角來說,包含Client、Server、ServiceManager以及binder驅動,其中ServiceManager用於管理系統中的各種服務。如圖:

可以看到,註冊服務、獲取服務、使用服務,都是需要經過binder通訊的。
-
Server通過註冊服務的Binder通訊把自己託管到ServiceManager
-
Client端可以通過ServiceManager獲取到Server
-
Client端獲取到Server後就可以使用Server的介面了
Binder通訊的代表類是BpBinder(客戶端)和BBinder(服務端)。
ps:有關binder的詳細知識,大家可以檢視Gityuan大佬的 Binder系列 [7] 文章。
ioctl函式
ioctl(input/output control)是一個專用於裝置輸入輸出操作的系統呼叫,它誕生在這樣一個背景下:
操作一個裝置的IO的傳統做法,是在裝置驅動程式中實現write的時候檢查一下是否有特殊約定的資料流通過,如果有的話,後面就跟著控制命令(socket程式設計中常常這樣做)。但是這樣做的話,會導致程式碼分工不明,程式結構混亂。所以就有了ioctl函式,專門向驅動層傳送或接收指令。
Linux作業系統分為了兩層,使用者層和核心層。我們的普通應用程式處於使用者層,系統底層程式,比如網路棧、裝置驅動程式,處於核心層。為了保證安全,作業系統要阻止使用者態的程式直接訪問核心資源。一個 Ioctl 介面是一個獨立的系統呼叫,通過它使用者空間可以跟裝置驅動溝通了。函式原型:
int ioctl(int fd, int request, …);
作用:通過IOCTL函式實現指令的傳遞
-
fd 是使用者程式開啟裝置時使用open函式返回的檔案描述符
-
request是使用者程式對裝置的控制命令
-
後面的省略號是一些補充引數,和cmd的意義相關
應用程式在呼叫 ioctl
進行裝置控制時,最後會呼叫到設備註冊 struct file_operations
結構體物件時的 unlocked_ioctl
或者 compat_ioctl
兩個鉤子上,例如Binder驅動的這兩個鉤子是掛到了binder_ioctl方法上:
static const struct file_operations binder_fops = {
.owner = THIS_MODULE,
.poll = binder_poll,
.unlocked_ioctl = binder_ioctl,
.compat_ioctl = binder_ioctl,
.mmap = binder_mmap,
.open = binder_open,
.flush = binder_flush,
.release = binder_release,
};
它的實現如下:
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
/*根據不同的命令,呼叫不同的處理函式進行處理*/
switch (cmd) {
case BINDER_WRITE_READ:
/*讀寫命令,資料傳輸,binder IPC通訊的核心邏輯*/
ret = **binder_ioctl_write_read**(filp, cmd, arg, thread);
break;
case BINDER_SET_MAX_THREADS:
/*設定最大執行緒數,直接將值設定到proc結構的max_threads域中。*/
break;
case BINDER_SET_CONTEXT_MGR:
/*設定Context manager,即將自己設定為ServiceManager,詳見3.3*/
break;
case BINDER_THREAD_EXIT:
/*binder執行緒退出命令,釋放相關資源*/
break;
case BINDER_VERSION: {
/*獲取binder驅動版本號,在kernel4.4版本中,32位該值為7,64位版本該值為8*/
break;
}
return ret;
}
具體核心層的實現,我們就不關心了。到這裡我們瞭解到,Binder在Android系統中會有一個裝置節點,呼叫ioctl控制這個節點時,實際上會呼叫到核心態的binder_ioctl方法。
為了利用ioctl啟動Android Service,必然是需要用ioctl向binder驅動寫資料,而這個控制命令就是 BINDER_WRITE_READ
。binder驅動層的一些細節我們在這裡就不關心了。那麼在什麼地方會用ioctl 向binder寫資料呢?
IPCThreadState.talkWithDriver
閱讀Gityuan的 Binder系列6—獲取服務(getService) [8] 一節,在binder模組下 IPCThreadState.cpp [9] 中有這樣的實現(原始碼目錄:frameworks/native/libs/binder/IPCThreadState.cpp):
status_t IPCThreadState::talkWithDriver(bool doReceive) {
...
binder_write_read bwr;
bwr.write_buffer = (uintptr_t)mOut.data();
status_t err;
do {
//通過ioctl不停的讀寫操作,跟Binder Driver進行通訊
if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
err = NO_ERROR;
...
} while (err == -EINTR); //當被中斷,則繼續執行
...
return err;
}
可以看到ioctl跟binder driver互動很簡單,一個引數是mProcess->mDriverFD,一個引數是BINDER_WRITE_READ,另一個引數是binder_write_read結構體,很幸運的是,NDK中提供了 linux/android/binder.h
這個標頭檔案,裡面就有binder_write_read這個結構體,以及BINDER_WRITE_READ常量的定義。
[驚不驚喜意不意外]
#include<linux/android/binder.h>
struct binder_write_read {
binder_size_t write_size;
binder_size_t write_consumed;
binder_uintptr_t write_buffer;
binder_size_t read_size;
binder_size_t read_consumed;
binder_uintptr_t read_buffer;
};
#define BINDER_WRITE_READ _IOWR('b', 1, struct binder_write_read)
這意味著,這些結構體和巨集定義很可能是版本相容的。
那我們只需要到時候把資料揌到binder_write_read結構體裡面,就可以進行ioctl系統呼叫了!
/dev/binder
再來看看mProcess->mDriverFD是什麼東西。mProcess也就是 ProcessState.cpp [10] (原始碼目錄:frameworks/native/libs/binder/ProcessState.cpp):
ProcessState::ProcessState(const char *driver)
: mDriverName(String8(driver))
, mDriverFD(open_driver(driver))
, ...
{}
從ProcessState的建構函式中得知,mDriverFD由open_driver方法初始化。
static int open_driver(const char *driver) {
int fd = open(driver, O_RDWR | O_CLOEXEC);
if (fd >= 0) {
int vers = 0;
status_t result = ioctl(fd, BINDER_VERSION, &vers);
}
return fd;
}
ProcessState在哪裡例項化呢?
sp<ProcessState> ProcessState::self() {
if (gProcess != nullptr) {
return gProcess;
}
gProcess = new ProcessState(kDefaultDriver);
return gProcess;
}
可以看到,ProcessState的gProcess是一個全域性單例物件,這意味著,在當前程序中,open_driver只會執行一次,得到的 mDriverFD 會一直被使用。
const char* kDefaultDriver = "/dev/binder";
而open函式操作的這個裝置節點就是/dev/binder。
納尼?在應用層直接操作裝置節點?Gityuan大佬不會騙我吧?一般來說,Android系統在整合SELinux的安全機制之後,普通應用甚至是系統應用,都不能直接操作一些裝置節點,除非有SELinux規則,給應用所屬的域或者角色賦予了那樣的許可權。
看看檔案許可權:
➜ ~ adb shell
chiron:/ $ ls -l /dev/binder
crw-rw-rw- 1 root root 10, 49 1972-07-03 18:46 /dev/binder
可以看到,/dev/binder裝置對所有使用者可讀可寫。
再看看,SELinux許可權:
chiron:/ $ ls -Z /dev/binder
u:object_r:binder_device:s0 /dev/binder
檢視原始碼中對binder_device角色的SELinux規則描述:
allow domain binder_device:chr_file rw_file_perms;
也就是所有domain對binder的字元裝置有讀寫許可權,而普通應用屬於domain。
既然這樣, 肝它!
寫個Demo試一下
驗證一下上面的想法,看看ioctl給binder driver發資料好不好使。
1、開啟裝置
int fd = open("/dev/binder", O_RDWR | O_CLOEXEC);
if (fd < 0) {
LOGE("Opening '%s' failed: %s\n", "/dev/binder", strerror(errno));
} else {
LOGD("Opening '%s' success %d: %s\n", "/dev/binder", fd, strerror(errno));
}
2、ioctl
Parcel *parcel = new Parcel;
parcel->writeString16(String16("test"));
binder_write_read bwr;
bwr.write_size = parcel->dataSize();
bwr.write_buffer = (binder_uintptr_t) parcel->data();
int ret = ioctl(fd, BINDER_WRITE_READ, bwr);
LOGD("ioctl result is %d: %s\n", ret, strerror(errno));
3、檢視日誌
D/KeepAlive: Opening '/dev/binder' success, fd is 35
D/KeepAlive: ioctl result is -1: Invalid argument
開啟裝置節點成功了,耶:v:!但是ioctl失敗了 ,失敗原因是 Invalid argument
,也就是說可以通訊,但是Parcel資料有問題。來看看資料應該是什麼樣的。
binder_write_read結構體資料封裝
IPCThreadState.talkWithDriver方法中,bwr.write_buffer指標指向了mOut.data(),顯然mOut是一個Parcel物件。
binder_write_read bwr;
bwr.write_buffer = (uintptr_t)mOut.data();
再來看看什麼時候會向mOut中寫資料:
status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,
int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer)
{
binder_transaction_data tr;
tr.data.ptr.buffer = data.ipcData();
...
mOut.writeInt32(cmd);
mOut.write(&tr, sizeof(tr));
return NO_ERROR;
}
writeTransactionData方法中,會往mOut中寫入一個binder_transaction_data結構體資料,binder_transaction_data結構體中又包含了作為引數傳進來的data Parcel物件。
writeTransactionData方法會被transact方法呼叫:
status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data,
Parcel* reply, uint32_t flags) {
status_t err = data.errorCheck(); // 資料錯誤檢查
flags |= TF_ACCEPT_FDS;
if (err == NO_ERROR) {
// 傳輸資料
err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);
}
...
// 預設情況下,都是採用非oneway的方式, 也就是需要等待服務端的返回結果
if ((flags & TF_ONE_WAY) == 0) {
if (reply) {
//等待迴應事件
err = waitForResponse(reply);
}else {
Parcel fakeReply;
err = waitForResponse(&fakeReply);
}
} else {
err = waitForResponse(NULL, NULL);
}
return err;
}
IPCThreadState是跟binder driver真正進行互動的類。每個執行緒都有一個 IPCThreadState
,每個 IPCThreadState
中都有一個mIn、一個mOut。成員變數mProcess儲存了ProcessState變數(每個程序只有一個)。
接著看一下一次Binder呼叫的時序圖:

Binder介紹一節中說過,BpBinder是Binder Client,上層想進行程序間Binder通訊時,會呼叫到BpBinder的transact方法,進而呼叫到IPCThreadState的transact方法。來看看BpBinder的transact方法的定義:
status_t BpBinder::transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {
if (mAlive) {
status_t status = IPCThreadState::self()->transact(mHandle, code, data, reply, flags);
if (status == DEAD_OBJECT) mAlive = 0;
return status;
}
return DEAD_OBJECT;
}
BpBinder::transact方法的code/data/reply/flags這幾個引數都是呼叫的地方傳過來的,現在唯一不知道的就是mHandle是什麼東西。mHandle是BpBinder(也就是Binder Client)的一個int型別的區域性變數(控制代碼),只要拿到了這個handle就相當於拿到了BpBinder。
ioctl啟動Service分幾步?
下面是在依賴libbinder.so時,啟動Service的步驟:
// 獲取ServiceManager
sp<IServiceManager> sm = defaultServiceManager();
// 獲取ActivityManager binder
sp<IBinder> binder = sm->getService(String16("activity"));
// 傳輸parcel
int result = binder.get()->transact(code, parcel, NULL, 0);
1、獲取到IServiceManager Binder Client;
2、從ServiceManager中獲取到ActivityManager Binder Client;
3、呼叫ActivityManager binder的transact方法傳輸Service的Parcel資料。
通過ioctl啟動Service也應該是類似的步驟:
1、獲取到ServiceManager的mHandle控制代碼;
2、進行binder呼叫獲取到ActivityManager的mHandle控制代碼;
3、進行binder呼叫傳輸啟動Service的指令資料。
這裡有幾個問題:
1、不依賴libbinder.so時,ndk中沒有Parcel類的定義,parcel資料哪裡來,怎麼封裝?
2、如何獲取到BpBinder的mHandle控制代碼?
如何封裝Parcel資料
Parcel類是Binder程序間通訊的一個基礎的、必不可少的資料結構,往Parcel中寫入的資料實際上是寫入到了一塊內部分配的記憶體上,最後把這個記憶體地址封裝到binder_write_read結構體中。Parcel作為一個基礎的資料結構,和Binder相關類是可以解耦的,可以直接拿過來使用,我們可以根據需要對有耦合性的一些方法進行裁剪。
c++ Parcel類路徑: frameworks [11] / native [12] / libs [13] / binder [14] / Parcel.cpp [15]
jni Parcel類路徑: frameworks [16] / base [17] / core [18] / jni [19] / android\_os\_Parcel.cpp [20]
如何獲取到BpBinder的mHandle控制代碼
具體流程參考 Binder系列4—獲取ServiceManager [21] 。
1、獲取ServiceManager的mHandle控制代碼
defaultServiceManager()方法用來獲取 gDefaultServiceManager
物件,gDefaultServiceManager是ServiceManager的單例。
sp<IServiceManager> defaultServiceManager() {
if (gDefaultServiceManager != NULL) return gDefaultServiceManager;
while (gDefaultServiceManager == NULL) {
gDefaultServiceManager = interface_cast<IServiceManager>(
ProcessState::self()->getContextObject(NULL));
}
}
return gDefaultServiceManager;
}
getContextObject方法用來獲取BpServiceManager物件(BpBinder),檢視其定義:
sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/) {
sp<IBinder> context = getStrongProxyForHandle(0);
return context;
}
可以發現,getStrongProxyForHandle是一個根據handle獲取IBinder物件的方法,而這裡handle的值為0,可以得知, ServiceManager的mHandle恆為0 。
2、獲取ActivityManager的mHandle控制代碼
獲取ActivityManager的c++方法是:
sp<IBinder> binder = serviceManager->getService(String16("activity"));
BpServiceManager.getService:
virtual sp<IBinder> getService(const String16& name) const {
sp<IBinder> svc = checkService(name);
if (svc != NULL) return svc;
return NULL;
}
BpServiceManager.checkService:
virtual sp<IBinder> checkService( const String16& name) const {
Parcel data, reply;
//寫入RPC頭
data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
//寫入服務名
data.writeString16(name);
remote()->transact(CHECK_SERVICE_TRANSACTION, data, &reply);
return reply.readStrongBinder();
}
可以看到,CHECK_SERVICE_TRANSACTION這個binder呼叫是有返回值的,返回值會寫到reply中,通過reply.readStrongBinder()方法,即可從reply這個Parcel物件中讀取到ActivityManager的IBinder。每個Binder物件必須要有它自己的mHandle控制代碼,不然,transact操作是沒辦法進行的。所以,很有可能,Binder的mHandle的值是寫到reply這個Parcel裡面的。
看看reply.readStrongBinder()方法搞了什麼鬼:
sp<IBinder> Parcel::readStrongBinder() const {
sp<IBinder> val;
readNullableStrongBinder(&val);
return val;
}
status_t Parcel::readNullableStrongBinder(sp<IBinder>* val) const {
return unflattenBinder(val);
}
呼叫到了Parcel::unflattenBinder方法,顧名思義,函式最終想要得到的是一個Binder物件,而Parcel中存放的是二進位制的資料,unflattenBinder很可能是把Parcel中的一個結構體資料給轉成Binder物件。
看看Parcel::unflattenBinder方法的定義:
status_t Parcel::unflattenBinder(sp<IBinder>* out) const {
const flat_binder_object* flat = readObject(false);
if (flat) {
...
sp<IBinder> binder =
ProcessState::self()->getStrongProxyForHandle(flat->handle);
}
return BAD_TYPE;
}
果然如此,從Parcel中可以得到一個flat_binder_object結構體,這個結構體重有一個handle變數,這個變數就是BpBinder中的mHandle控制代碼。
因此,在不依賴libbinder.so的情況下,我們可以自己組裝資料傳送給ServiceManager,進而獲取到ActivityManager的mHandle控制代碼。
IPCThreadState是一個被Binder依賴的類,它是可以從原始碼中抽離出來為我們所用的。上一節中說到,Parcel類也是可以從原始碼中抽離出來的。
通過如下的操作,我們就可以實現ioctl獲取到ActivityManager對應的Parcel物件reply:
Parcel data, reply;
// data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
// IServiceManager::getInterfaceDescriptor()的值是android.app.IActivityManager
data.writeInterfaceToken(String16("android.app.IActivityManager"));
data.writeString16(String16("activity"));
IPCThreadState::self()->transact(
0/*ServiceManger的mHandle控制代碼恆為0*/,
CHECK_SERVICE_TRANSACTION, data, reply, 0);
reply變數也就是我們想要的包含了flat_binder_object結構體的Parcel物件,再經過如下的操作就可以得到ActivityManager的mHandle控制代碼:
const flat_binder_object* flat = reply->readObject(false);
return flat->handle;
3、傳輸啟動指定Service的Parcel資料
上一步已經拿到ActivityManger的mHandle控制代碼,比如值為1。這一步的過程和上一步類似,自己封裝Parcel,然後呼叫IPCThreadState::transact方法傳輸資料,虛擬碼如下:
Parcel data;
// 把Service相關資訊寫到parcel中
writeService(data, packageName, serviceName, sdk_version);
IPCThreadState::self()->transact(
1/*上一步獲取的ActivityManger的mHandle控制代碼值是1*/,
CHECK_SERVICE_TRANSACTION, data, reply,
1/*TF_ONE_WAY*/);
4、writeService方法需要做什麼事情?
下面這段程式碼是Java中封裝Parcel物件的方法:
Intent intent = new Intent();
ComponentName component = new ComponentName(context.getPackageName(), serviceName);
intent.setComponent(component);
Parcel mServiceData = Parcel.obtain();
mServiceData.writeInterfaceToken("android.app.IActivityManager");
mServiceData.writeStrongBinder(null);
mServiceData.writeInt(1);
intent.writeToParcel(mServiceData, 0);
mServiceData.writeString(null); // resolvedType
mServiceData.writeInt(0);
mServiceData.writeString(context.getPackageName()); // callingPackage
mServiceData.writeInt(0);
可以看到,有Intent類轉Parcel,ComponentName類轉Parcel,這些類在c++中是沒有對應的類的。所以需要我們參考 intent.writeToParcel
/ ComponentName.writeToParcel
等方法的原始碼的實現,自行封裝資料。下面這段程式碼就是把啟動Service的Intent寫到Parcel中的方法:
void writeIntent(Parcel &out, const char *mPackage, const char *mClass) {
// mAction
out.writeString16(NULL, 0);
// uri mData
out.writeInt32(0);
// mType
out.writeString16(NULL, 0);
// // mIdentifier
out.writeString16(NULL, 0);
// mFlags
out.writeInt32(0);
// mPackage
out.writeString16(NULL, 0);
// mComponent
out.writeString16(String16(mPackage));
out.writeString16(String16(mClass));
// mSourceBounds
out.writeInt32(0);
// mCategories
out.writeInt32(0);
// mSelector
out.writeInt32(0);
// mClipData
out.writeInt32(0);
// mContentUserHint
out.writeInt32(-2);
// mExtras
out.writeInt32(-1);
}
繼續寫Demo試一下
上面已經知道了怎麼通過ioctl獲取到ActivityManager,可以寫demo試一下:
// 開啟binder裝置
int fd = open("/dev/binder", O_RDWR | O_CLOEXEC);
Parcel data, reply;
// data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
// IServiceManager::getInterfaceDescriptor()的值是android.app.IActivityManager
data.writeInterfaceToken(String16("android.app.IActivityManager"));
data.writeString16(String16("activity"));
IPCThreadState::self()->transact(
0/*ServiceManger的mHandle控制代碼恆為0*/,
CHECK_SERVICE_TRANSACTION, data, reply, 0);
const flat_binder_object *flat = reply->readObject(false);
if (flat) {
LOGD("write_transact handle is:%llu", flat->handle);
}else {
LOGD("write_transact failed, error=%d", status);
}
給IPCThreadState::transact加上一些日誌,列印結果如下:
D/KeepAlive: BR_DEAD_REPLY
D/KeepAlive: write_transact failed, error=-32
reply中始終讀不到資料。這是為什麼?現在已經不報 Invalid argument
的錯誤了,說明Parcel資料格式可能沒問題了。但是不能成功把資料寫給ServiceManager,或者ServiceManager返回的資料不能成功寫回來。
想到Binder是基於記憶體的一種IPC機制,資料都是對的,那問題就出在記憶體上了。這就要說到Binder基本原理以及Binder記憶體轉移關係。
Binder基本原理:

Binder的Client端和Server端位於不同的程序,它們的使用者空間是相互隔離。而核心空間由Linux核心程序來維護,在安全性上是有保障的。所以,Binder的精髓就是在核心態開闢了一塊共享記憶體。

資料傳送方寫資料時,核心態通過copy_from_user()方法把它的資料拷貝到資料接收方對映(mmap)到核心空間的地址上。這樣,只需要一次資料拷貝過程,就可以完成程序間通訊。
由此可知,沒有這塊核心空間是沒辦法完成IPC通訊的。Demo失敗的原因就是缺少了一個mmap過程,以對映一塊記憶體到核心空間。修改如下:
#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)
int mDriverFD = open("/dev/binder", O_RDWR | O_CLOEXEC);
mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
日誌:
D/KeepAlive: BR_REPLY
D/KeepAlive: write_transact handle is:1
搞定!
最後
相關的程式碼我已經發布到Github( lcodecorex
[22]
/
KeepAlive
[23]
), master
分支是 利用 libbinder.so 與 ActivityManagerService 通訊
的版本, ioctl
分支是 使用 ioctl 與 binder 驅動通訊
的版本。
當然,這個保活的辦法雖然很強,但現在也 只能活在模擬器裡 了。
說一下我的方法論。
1、確定問題和目標。
研究一個比較複雜的東西的時候,我們比較難有一個大局觀。這個時候,就需要明確自己需要什麼?有問題,才能推動自己學習,然後順騰摸瓜,最後弄清自己的模組在系統中的位置。
這篇文章,我們確定了目標是直接通過ioctl進行Binder通訊,進而確定Binder通訊的關鍵是拿到mHandle控制代碼。同時也理清了Binder通訊的一個基本流程。
2、時序圖很重要。
大佬們畫的時序圖,可快幫助我們快速理清框架的思路。
3、實踐出真知。
紙上得來終覺淺,絕知此事要躬行。我一直踐行的一個學習方式是學以致用,可以及時寫Demo幫助我們鞏固知識以及分析問題。
參考資料:
參考資料比較多,具體見原文:
https://juejin.cn/post/6844904110219608078
·················END·················
推薦閱讀
為了防止失聯,歡迎關注我的小號
微信改了推送機制,真愛請星標本公號 :point_down:
- 說兩件事~
- 最新的動畫布局來了,一文帶你瞭解!
- Gradle:你必須掌握的開發常見技巧~
- Kotlin DSL 實戰:像 Compose 一樣寫程式碼!
- 厲害了,Android自定義樹狀圖控制元件來了!
- 一文帶你全面掌握Android元件化核心!
- 為什麼大廠開始全面轉向Compose?
- 谷歌限制俄羅斯使用Android系統,俄或將轉用 HarmonyOS!
- 鴻蒙OS、安卓、iOS測試對比,結果出乎意料!
- 最詳細的Android圖片壓縮攻略,讓你一次過足癮(建議收藏)
- Android字型漸變效果實戰!
- 攔截控制元件點選 - 巧用ASM處理防抖!
- Android正確的保活方案,拒絕陷入需求死迴圈!
- 再見 MMKV,自己擼一個FastKV,快的一批
- 白嫖一個Android專案的類圖生成工具!(建議收藏)
- 日常需求做的挺好,面試就被底層原理放倒
- 40歲開始學習Android開發,現在成了一名技術主管
- Android效能優化:全量編譯提速黑科技!
- 華為再次甩出“王炸”:鴻蒙終於“上車”
- 眼瞅著就要過年了,程式設計師們也都按奈不住了了