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

後來,爲數很少的維術大佬在Gityuan大佬的基礎上,發佈了博客Android 黑科技保活實現原理揭祕又進行了系統進程查殺相關的源碼分析。爲咱們帶來的結論是,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,所以這四個進程三三之間造成了鐵三角,從而保證了存活率。shell

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

維術大佬寫了一個簡單的實現,代碼在這裏:github.com/tiann/Leori…,這個方案是當檢測到進程被殺時,會經過JNI的方式,調用Java層的方法來複活進程。爲了實現穩定的保活,尤爲是系統殺進程只給了5ms復活的機會,使用JNI這種方式復活進程如今達不到最優的效果。windows

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的源碼能夠看到,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.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.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系列文章。

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)一節,在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系統調用了!

/dev/binder

再來看看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。

既然這樣,肝它!

寫個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/native/libs/binder/Parcel.cpp

jni Parcel類路徑:frameworks/base/core/jni/android_os_Parcel.cpp

如何獲取到BpBinder的mHandle句柄

具體流程參考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);
}

複製代碼

繼續寫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/KeepAlive),master分支是利用 libbinder.so 與 ActivityManagerService 通訊的版本,ioctl分支是使用 ioctl 與 binder 驅動通訊的版本。

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

說一下個人方法論。

一、肯定問題和目標。

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

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

二、時序圖很重要。

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

三、實踐出真知。

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

鳴謝

Gityuan大佬的Binder系列

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

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

Binder驅動之設備控制binder_ioctl

Google官方Android源碼瀏覽網站

相關文章
相關標籤/搜索