Android 最強保活黑科技的最強技術實現

你們好,我是老玩童。今天來跟你們分享 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 的進程永生技術 [1](後來不知道什麼緣由又刪除了),頓時間掀起了一陣波瀾,彷彿讓開發者們又看到了應用保活的一絲但願。Gityuan 大佬經過超強的專業技術分析,爲咱們解開了 TIM 保活方案的終極奧義。c++

後來,爲數很少的維術大佬在 Gityuan 大佬的基礎上,發佈了博客 Android 黑科技保活實現原理揭祕 [2] 又進行了系統進程查殺相關的源碼分析。爲咱們帶來的結論是,Android 系統殺應用的時候,會去殺進程組,循環 40 遍不停地殺進程,每次殺完以後等 5msgit

總之,引用維術的話語,原理以下:github

  1. 利用 Linux 文件鎖的原理,使用 2 個進程互相監聽各自的文件鎖,來感知彼此的死亡。
  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,所以這四個進程三三之間造成了鐵三角,從而保證了存活率。web

按照維術大佬的理論,只要進程我復活的足夠快,系統它就殺不死我,嘿嘿。shell

維術大佬寫了一個簡單的實現,代碼在這裏:github.com/tiann/Leoric[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 通訊,以喚醒新進程。

  1. Java 層建立 Parcel (含 Intent),拿到 Parcel 對象的 mNativePtr(native peer),傳到 Native 層。
  2. native 層直接把 mNativePtr 強轉爲結構體指針。
  3. fork 子進程,創建管道,準備傳輸 parcel 數據。
  4. 子進程讀管道,拿到二進制流,重組爲 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 發數據好很差使。

一、打開設備

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 數據有問題。來看看數據應該是什麼樣的。

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);

一、獲取到 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 數據

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]

一、獲取 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);
}

繼續寫 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 驅動通訊的版本。

固然,這個保活的辦法雖然很強,但如今也只能活在模擬器裏了。

說一下個人方法論。

一、肯定問題和目標。

研究一個比較複雜的東西的時候,咱們比較難有一個大局觀。這個時候,就須要明確本身須要什麼?有問題,才能推進本身學習,而後順騰摸瓜,最後弄清本身的模塊在系統中的位置。

這篇文章,咱們肯定了目標是直接經過 ioctl 進行 Binder 通訊,進而肯定 Binder 通訊的關鍵是拿到 mHandle 句柄。同時也理清了 Binder 通訊的一個基本流程。

二、時序圖很重要。

大佬們畫的時序圖,可快幫助咱們快速理清框架的思路。

三、實踐出真知。

紙上得來終覺淺,絕知此事要躬行。我一直踐行的一個學習方式是學以至用,能夠及時寫 Demo 幫助咱們鞏固知識以及分析問題。

鳴謝

Gityuan 大佬的Binder 系列 [24]

Android 黑科技保活實現原理揭祕 [25]

binder Driver (binder IPC) 功能介紹與分析 [26]

Binder 驅動之設備控制`binder_ioctl`[27]

Google 官方 Android 源碼瀏覽網站 [28]

做者:小玩童 

連接:https://juejin.im/post/5e820b61e51d45470652e7b8

參考資料

[1]

史上最強 Android 保活思路:深刻剖析騰訊 TIM 的進程永生技術: http://www.52im.net/forum.php?mod=viewthread&tid=2893&highlight=%B1%A3%BB%EE

[2]

Android 黑科技保活實現原理揭祕: http://weishu.me/2020/01/16/a-keep-alive-method-on-android/

[3]

github.com/tiann/Leoric: https://github.com/tiann/Leoric

[4]

Parcel 的源碼: https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=336

[5]

Parcel 類: https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/Parcel.cpp

[6]

Google Factory Images: https://developers.google.cn/android/images?h1=zh=cn#angler

[7]

Binder 系列: http://gityuan.com/2015/10/31/binder-prepare/

[8]

Binder 系列 6—獲取服務 (getService): http://gityuan.com/2015/11/15/binder-get-service/

[9]

IPCThreadState.cpp: https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/IPCThreadState.cpp

[10]

ProcessState.cpp: https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/ProcessState.cpp

[11]

frameworks: https://cs.android.com/android/platform/superproject/+/master:frameworks/

[12]

native: https://cs.android.com/android/platform/superproject/+/master:frameworks/native/

[13]

libs: https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/

[14]

binder: https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/

[15]

Parcel.cpp: https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/Parcel.cpp

[16]

frameworks: https://cs.android.com/android/platform/superproject/+/master:frameworks/

[17]

base: https://cs.android.com/android/platform/superproject/+/master:frameworks/base/

[18]

core: https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/

[19]

jni: https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/jni/

[20]

android_os_Parcel.cpp: https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/jni/android_os_Parcel.cpp

[21]

Binder 系列 4—獲取 ServiceManager: http://gityuan.com/2015/11/08/binder-get-sm/

[22]

lcodecorex: https://github.com/lcodecorex

[23]

KeepAlive: https://github.com/lcodecorex/KeepAlive

[24]

Binder 系列: http://gityuan.com/2015/10/31/binder-prepare/

[25]

Android 黑科技保活實現原理揭祕: http://weishu.me/2020/01/16/a-keep-alive-method-on-android/

[26]

binder Driver (binder IPC) 功能介紹與分析: https://blog.csdn.net/vshuang/article/details/88823044

[27]

Binder 驅動之設備控制binder_ioctl: https://www.jianshu.com/p/49830c3473b7

[28]

Google 官方 Android 源碼瀏覽網站: https://cs.android.com/


本文分享自微信公衆號 - 賈小昆(zywudev)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索