作者:小玩童連結:juejin.im/post/5e820b61e51d45470652e7b8版權說明:本文為小頑童大佬投稿,轉載請聯繫作者
大家好,我是老玩童。今天來跟大家分享TIM最強保活思路的幾種實現方法。這篇文章我將通過ioctl跟binder驅動交互,實現以最快的方式喚醒新的保活服務,最大程度防止保活失敗。同時,我也將跟您分享,我是怎麼做到在不甚了解binder的情況下,快速實現ioctl binder這種高級操作。
隨著Android陣營的各大手機廠商對於續航的高度重視,兩三年前的手機發布會更是把反保活作為一個系統的賣點,不斷提出了各種反保活的方案,導致現在想實現應用保活簡直難於上青天,甚至都需要一個團隊來專門研究這個事情。連微信這種超級APP,也要拜倒在反保活的石榴裙下,允許後臺啟動太費電,不允許後臺啟動就收不到消息。。Android發現了一個保活野路子就堵一條,然而很多場景是有保活的強需求的,有木有考慮過我們開發者的感受,自己人何必為難自己人。
我覺得這是一個Android設計的不合理的地方,路子可以堵,但還是有必要留一個統一的保活接口的。這個接口由Google實現也好,廠商來實現也好,總好過現在很笨拙的系統自啟動管理或者是JobScheduler。我覺得本質上來說,讓應用開發者想盡各種辦法去做保活,這個事情是沒有意義的,保活的路子被封了,但保活還是需要做,保活的成本也提高了,簡直浪費生命。(僅代表個人觀點)
黑科技進程保活原理
大概2個月前,Gityuan大佬放出了一份分析TIM的黑科技保活的博客史上最強Android保活思路:深入剖析騰訊TIM的進程永生技術(後來不知道什麼原因又刪除了),頓時間掀起了一陣波瀾,仿佛讓開發者們又看到了應用保活的一絲希望。Gityuan大佬通過超強的專業技術分析,為我們解開了TIM保活方案的終極奧義。
後來,為數不多的維術大佬在Gityuan大佬的基礎上,發布了博客Android 黑科技保活實現原理揭秘又進行了系統進程查殺相關的源碼分析。為我們帶來的結論是,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…,這個方案是當檢測到進程被殺時,會通過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); // resolvedTypemServiceData.writeInt(0);mServiceData.writeString(context.getPackageName()); // callingPackagemServiceData.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); // resolvedTypemServiceData.writeInt(0);mServiceData.writeString(context.getPackageName()); // callingPackagemServiceData.writeInt(0);查看Parcel的源碼可以看到,Parcel類有一個mNativePtr變量:
privatelong mNativePtr; // used by native code// android4.4 mNativePtr是int類型可以通過反射得到這個變量:
privatestaticlonggetNativePtr(Parcel parcel) {try { Field ptrField = parcel.getClass().getDeclaredField("mNativePtr"); ptrField.setAccessible(true);return (long) ptrField.get(parcel); } catch (Exception e) { e.printStackTrace(); }return0;}這個變量對應了C++中Parcel類的地址,因此可以強轉得到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 網頁裡找到適合的版本,下載相應的固件,然後解包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.sotarget_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);} elseif (pid == 0) {//第一個子進程if ((pid = fork()) < 0) {exit(-1); } elseif (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:
Parcelparcel;parcel.setData(data, data_size);傳輸Parcel數據
// 獲取ServiceManagersp<IServiceManager> sm = defaultServiceManager();// 獲取ActivityManager bindersp<IBinder> binder = sm->getService(String16("activity"));// 傳輸parcelint 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通信把自己託管到ServiceManagerClient端可以通過ServiceManager獲取到ServerClient端獲取到Server後就可以使用Server的接口了Binder通信的代表類是BpBinder(客戶端)和BBinder(服務端)。
ps:有關binder的詳細知識,大家可以查看Gityuan大佬的Binder系列文章。
ioctl函數
ioctl(input/output control)是一個專用於設備輸入輸出操作的系統調用,它誕生在這樣一個背景下:
操作一個設備的IO的傳統做法,是在設備驅動程序中實現write的時候檢查一下是否有特殊約定的數據流通過,如果有的話,後面就跟著控制命令(socket編程中常常這樣做)。但是這樣做的話,會導致代碼分工不明,程序結構混亂。所以就有了ioctl函數,專門向驅動層發送或接收指令。
Linux作業系統分為了兩層,用戶層和內核層。我們的普通應用程式處於用戶層,系統底層程序,比如網絡棧、設備驅動程序,處於內核層。為了保證安全,作業系統要阻止用戶態的程序直接訪問內核資源。一個Ioctl接口是一個獨立的系統調用,通過它用戶空間可以跟設備驅動溝通了。函數原型:
intioctl(int fd, int request, …);作用:通過IOCTL函數實現指令的傳遞
fd 是用戶程序打開設備時使用open函數返回的文件描述符request是用戶程序對設備的控制命令後面的省略號是一些補充參數,和cmd的意義相關應用程式在調用ioctl進行設備控制時,最後會調用到設備註冊struct file_operations結構體對象時的unlocked_ioctl或者compat_ioctl兩個鉤子上,例如Binder驅動的這兩個鉤子是掛到了binder_ioctl方法上:
staticconststructfile_operationsbinder_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,};它的實現如下:
staticlongbinder_ioctl(struct file *filp, unsignedint cmd, unsignedlong 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)一節,在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>structbinder_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(源碼目錄:frameworks/native/libs/binder/ProcessState.cpp):
ProcessState::ProcessState(constchar *driver) : mDriverName(String8(driver)) , mDriverFD(open_driver(driver)) , ... {}從ProcessState的構造函數中得知,mDriverFD由open_driver方法初始化。
staticintopen_driver(constchar *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 會一直被使用。
constchar* kDefaultDriver = "/dev/binder";而open函數操作的這個設備節點就是/dev/binder。
納尼?在應用層直接操作設備節點?Gityuan大佬不會騙我吧?一般來說,Android系統在集成SELinux的安全機制之後,普通應用甚至是系統應用,都不能直接操作一些設備節點,除非有SELinux規則,給應用所屬的域或者角色賦予了那樣的權限。
看看文件權限:
~ adb shellchiron:/ $ ls -l /dev/bindercrw-rw-rw- 1 root root 10, 491972-07-0318:46 /dev/binder可以看到,/dev/binder設備對所有用戶可讀可寫。
再看看,SELinux權限:
chiron:/ $ ls -Z /dev/binderu: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 is35D/KeepAlive: ioctl result is-1: Invalid argument打開設備節點成功了,耶!但是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的步驟:
// 獲取ServiceManagersp<IServiceManager> sm = defaultServiceManager();// 獲取ActivityManager bindersp<IBinder> binder = sm->getService(String16("activity"));// 傳輸parcelint 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/native/libs/binder/Parcel.cpp
jni Parcel類路徑:frameworks/base/core/jni/android_os_Parcel.cpp
如何獲取到BpBinder的mHandle句柄
具體流程參考Binder系列4—獲取ServiceManager。
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;returnNULL;}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);returnval;}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.IActivityManagerdata.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); // resolvedTypemServiceData.writeInt(0);mServiceData.writeString(context.getPackageName()); // callingPackagemServiceData.writeInt(0);可以看到,有Intent類轉Parcel,ComponentName類轉Parcel,這些類在c++中是沒有對應的類的。所以需要我們參考intent.writeToParcel/ComponentName.writeToParcel等方法的源碼的實現,自行封裝數據。下面這段代碼就是把啟動Service的Intent寫到Parcel中的方法:
void writeIntent(Parcel &out, constchar *mPackage, constchar *mClass) {// mActionout.writeString16(NULL, 0);// uri mDataout.writeInt32(0);// mTypeout.writeString16(NULL, 0);// // mIdentifierout.writeString16(NULL, 0);// mFlagsout.writeInt32(0);// mPackageout.writeString16(NULL, 0);// mComponentout.writeString16(String16(mPackage));out.writeString16(String16(mClass));// mSourceBoundsout.writeInt32(0);// mCategoriesout.writeInt32(0);// mSelectorout.writeInt32(0);// mClipDataout.writeInt32(0);// mContentUserHintout.writeInt32(-2);// mExtrasout.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.IActivityManagerdata.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_REPLYD/KeepAlive: write_transact failed, error=-32reply中始終讀不到數據。這是為什麼?現在已經不報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_REPLYD/KeepAlive: write_transact handle is:1搞定!
最後
相關的代碼我已經發布到Github(lcodecorex/KeepAlive),master分支是利用 libbinder.so 與 ActivityManagerService 通信的版本,ioctl分支是使用 ioctl 與 binder 驅動通信的版本。
當然,這個保活的辦法雖然很強,但現在也只能活在模擬器裡了。
說一下我的方法論。
1、確定問題和目標。
研究一個比較複雜的東西的時候,我們比較難有一個大局觀。這個時候,就需要明確自己需要什麼?有問題,才能推動自己學習,然後順騰摸瓜,最後弄清自己的模塊在系統中的位置。
這篇文章,我們確定了目標是直接通過ioctl進行Binder通信,進而確定Binder通信的關鍵是拿到mHandle句柄。同時也理清了Binder通信的一個基本流程。
2、時序圖很重要。
大佬們畫的時序圖,可快幫助我們快速理清框架的思路。
3、實踐出真知。
紙上得來終覺淺,絕知此事要躬行。我一直踐行的一個學習方式是學以致用,可以及時寫Demo幫助我們鞏固知識以及分析問題。
鳴謝
Gityuan大佬的[Binder系列http://gityuan.com/2015/10/31/binder-prepare/
Android 黑科技保活實現原理揭秘http://weishu.me/2020/01/16/a-keep-alive-method-on-android/
binder Driver (binder IPC) 功能介紹與分析https://blog.csdn.net/vshuang/article/details/88823044
Binder驅動之設備控制binder_ioctlhttps://www.jianshu.com/p/49830c3473b7
Google官方Android源碼瀏覽網站https://cs.android.com/