你們好,我是老玩童。今天來跟你們分享TIM最強保活思路的幾種實現方法。這篇文章我將經過ioctl跟binder驅動交互,實現以最快的方式喚醒新的保活服務,最大程度防止保活失敗。同時,我也將跟您分享,我是怎麼作到在不甚瞭解binder的狀況下,快速實現ioctl binder這種高級操做。php
聲明:如今這個保活方式在MIUI等定製Android系統中已經不能保活,大部分時候只能活在模擬器中了。但對與咱們的輕量定製的Android系統,一些系統級應用的保活,這個方案仍是有用的。java
隨着Android陣營的各大手機廠商對於續航的高度重視,兩三年前的手機發佈會更是把反保活做爲一個系統的賣點,不斷提出了各類反保活的方案,致使如今想實現應用保活簡直難於上青天,甚至都須要一個團隊來專門研究這個事情。連微信這種超級APP,也要拜倒在反保活的石榴裙下,容許後臺啓動太費電,不容許後臺啓動就收不到消息。。Android發現了一個保活野路子就堵一條,然而不少場景是有保活的強需求的,有木有考慮過咱們開發者的感覺,本身人何須爲難本身人😭。linux
我以爲這是一個Android設計的不合理的地方,路子能夠堵,但仍是有必要留一個統一的保活接口的。這個接口由Google實現也好,廠商來實現也好,總好過如今很笨拙的系統自啓動管理或者是JobScheduler。我以爲本質上來講,讓應用開發者想盡各類辦法去作保活,這個事情是沒有意義的,保活的路子被封了,但保活仍是須要作,保活的成本也提升了,簡直浪費生命。Android的鍋。(僅表明我的觀點)android
大概2個月前,Gityuan大佬放出了一份分析TIM的黑科技保活的博客史上最強Android保活思路:深刻剖析騰訊TIM的進程永生技術(後來不知道什麼緣由又刪除了),頓時間掀起了一陣波瀾,彷彿讓開發者們又看到了應用保活的一絲但願。Gityuan大佬經過超強的專業技術分析,爲咱們解開了TIM保活方案的終極奧義。c++
後來,爲數很少的維術大佬在Gityuan大佬的基礎上,發佈了博客Android 黑科技保活實現原理揭祕又進行了系統進程查殺相關的源碼分析。爲咱們帶來的結論是,Android系統殺應用的時候,會去殺進程組,循環 40 遍不停地殺進程,每次殺完以後等 5ms。git
總之,引用維術的話語,原理以下:github
具體來講,建立 2 個進程 p1, p2,這兩個進程經過文件鎖互相關聯,一個被殺以後拉起另一個;同時 p1 通過 2 次 fork 產生孤兒進程 c1,p2 通過 2 次 fork 產生孤兒進程 c2,c1 和 c2 之間創建文件鎖關聯。這樣假設 p1 被殺,那麼 p2 會立馬感知到,而後 p1 和 c1 同屬一個進程組,p1 被殺會觸發 c1 被殺,c1 死後 c2 立馬感覺到從而拉起 p1,所以這四個進程三三之間造成了鐵三角,從而保證了存活率。shell
按照維術大佬的理論,只要進程我復活的足夠快,系統它就殺不死我,嘿嘿。編程
維術大佬寫了一個簡單的實現,代碼在這裏:github.com/tiann/Leori…,這個方案是當檢測到進程被殺時,會經過JNI的方式,調用Java層的方法來複活進程。爲了實現穩定的保活,尤爲是系統殺進程只給了5ms復活的機會,使用JNI這種方式復活進程如今達不到最優的效果。windows
復活進程,其實就是啓動指定的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);
複製代碼
在Java層作進程復活的工做,這個方式是比較低效的,最好的方式是在 native 層使用純 C/C++來複活進程。方案有兩個。
其一,維術大佬給出的方案是利用libbinder.so, 利用Android提供的C++接口,跟ActivityManagerService通訊,以喚醒新進程。
其二,Gityuan大佬則認爲使用 ioctl 直接給 binder 驅動發送數據以喚醒進程,纔是更高效的作法。然而,這個方法,大佬們並無提供思路。
那麼今天,咱們就來實現這兩種在 native 層進行 Binder 調用的騷操做。
上面在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的源碼能夠看到,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類的地址,所以能夠強轉獲得Parcel指針:
Parcel *parcel = (Parcel *) parcel_ptr;
複製代碼
然而,NDK中並無提供binder這個模塊,咱們只能從Android源碼中扒到binder相關的源碼,再編譯出libbinder.so。騰訊TIM應該就是魔改了binder相關的源碼。
爲了不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 網頁裏找到適合的版本,下載相應的固件,而後解包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的方式我也能夠嘗試着實現一下。ioctl是一個linux標準方法,那麼咱們就直奔主題看看,binder是什麼,ioctl怎麼跟binder driver通訊。
Binder是Android系統提供的一種IPC機制。每一個Android的進程,均可以有一塊用戶空間和內核空間。用戶空間在不一樣進程間不能共享,內核空間能夠共享。Binder就是一個利用能夠共享的內核空間,完成高性能的進程間通訊的方案。
Binder通訊採用C/S架構,從組件視角來講,包含Client、Server、ServiceManager以及binder驅動,其中ServiceManager用於管理系統中的各類服務。如圖:
能夠看到,註冊服務、獲取服務、使用服務,都是須要通過binder通訊的。
Binder通訊的表明類是BpBinder(客戶端)和BBinder(服務端)。
ps:有關binder的詳細知識,你們能夠查看Gityuan大佬的Binder系列文章。
ioctl(input/output control)是一個專用於設備輸入輸出操做的系統調用,它誕生在這樣一個背景下:
操做一個設備的IO的傳統作法,是在設備驅動程序中實現write的時候檢查一下是否有特殊約定的數據流經過,若是有的話,後面就跟着控制命令(socket編程中經常這樣作)。可是這樣作的話,會致使代碼分工不明,程序結構混亂。因此就有了ioctl函數,專門向驅動層發送或接收指令。
Linux操做系統分爲了兩層,用戶層和內核層。咱們的普通應用程序處於用戶層,系統底層程序,好比網絡棧、設備驅動程序,處於內核層。爲了保證安全,操做系統要阻止用戶態的程序直接訪問內核資源。一個Ioctl接口是一個獨立的系統調用,經過它用戶空間能夠跟設備驅動溝通了。函數原型:
int ioctl(int fd, int request, …);
複製代碼
做用:經過IOCTL函數實現指令的傳遞
應用程序在調用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寫數據呢?
閱讀Gityuan的Binder系列6—獲取服務(getService)一節,在binder模塊下IPCThreadState.cpp中有這樣的實現(源碼目錄: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系統調用了!
再來看看mProcess->mDriverFD是什麼東西。mProcess也就是ProcessState.cpp(源碼目錄: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。
既然這樣,肝它!
驗證一下上面的想法,看看ioctl給binder driver發數據好很差使。
一、打開設備
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));
}
複製代碼
二、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));
複製代碼
三、查看日誌
D/KeepAlive: Opening '/dev/binder' success, fd is 35
D/KeepAlive: ioctl result is -1: Invalid argument
複製代碼
打開設備節點成功了,耶✌️!可是ioctl失敗了🤔,失敗緣由是Invalid argument
,也就是說能夠通訊,可是Parcel數據有問題。來看看數據應該是什麼樣的。
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。
下面是在依賴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);
複製代碼
一、獲取到IServiceManager Binder Client;
二、從ServiceManager中獲取到ActivityManager Binder Client;
三、調用ActivityManager binder的transact方法傳輸Service的Parcel數據。
經過ioctl啓動Service也應該是相似的步驟:
一、獲取到ServiceManager的mHandle句柄;
二、進行binder調用獲取到ActivityManager的mHandle句柄;
三、進行binder調用傳輸啓動Service的指令數據。
這裏有幾個問題:
一、不依賴libbinder.so時,ndk中沒有Parcel類的定義,parcel數據哪裏來,怎麼封裝?
二、如何獲取到BpBinder的mHandle句柄?
Parcel類是Binder進程間通訊的一個基礎的、必不可少的數據結構,往Parcel中寫入的數據其實是寫入到了一塊內部分配的內存上,最後把這個內存地址封裝到binder_write_read結構體中。Parcel做爲一個基礎的數據結構,和Binder相關類是能夠解耦的,能夠直接拿過來使用,咱們能夠根據須要對有耦合性的一些方法進行裁剪。
c++ Parcel類路徑:frameworks/native/libs/binder/Parcel.cpp
jni Parcel類路徑:frameworks/base/core/jni/android_os_Parcel.cpp
具體流程參考Binder系列4—獲取ServiceManager。
一、獲取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。
二、獲取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;
複製代碼
三、傳輸啓動指定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*/);
複製代碼
四、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);
}
複製代碼
上面已經知道了怎麼經過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/KeepAlive),master
分支是利用 libbinder.so 與 ActivityManagerService 通訊
的版本,ioctl
分支是使用 ioctl 與 binder 驅動通訊
的版本。
固然,這個保活的辦法雖然很強,但如今也只能活在模擬器裏了。
說一下個人方法論。
一、肯定問題和目標。
研究一個比較複雜的東西的時候,咱們比較難有一個大局觀。這個時候,就須要明確本身須要什麼?有問題,才能推進本身學習,而後順騰摸瓜,最後弄清本身的模塊在系統中的位置。
這篇文章,咱們肯定了目標是直接經過ioctl進行Binder通訊,進而肯定Binder通訊的關鍵是拿到mHandle句柄。同時也理清了Binder通訊的一個基本流程。
二、時序圖很重要。
大佬們畫的時序圖,可快幫助咱們快速理清框架的思路。
三、實踐出真知。
紙上得來終覺淺,絕知此事要躬行。我一直踐行的一個學習方式是學以至用,能夠及時寫Demo幫助咱們鞏固知識以及分析問題。
Gityuan大佬的Binder系列