JNI與底層調用2

JNI開發系列閱讀

1. 掌握如何使用廣播接收者攔截電話和短信

在日常生活中,當手機丟失后,我們可以啟動一系列的措施來獲取丟失手機的位置信息,或者清楚丟失手機的數據來防止隱私數據泄露,本章就針對這些防盜保護措施進行講解。

2. JNI 開發中常見錯誤

2.1 動態庫名稱寫錯,或者不存在

static{
    System.loadLibrary("hell0");
}

當我們在寫上面代碼的時候如果不小心將hello 寫成了hell0。或者libhello.so 動態庫不存在,那么系統啟動時會報如下異常。

jni

2.2 Android.mk 配置文件寫錯

如果修改配置文件中的某個參數名稱被寫錯,那么我們在調用ndk-build.cmd 命令的時候很可能報如下異常。出現這類異常,就需要我們檢查我們的Android.mk 文件,看是否寫錯。

jni

2.3 目標文件名畫蛇添足導致的錯誤

手機防盜界面會展示出一系列的信息,如用戶設置的安全號碼、手機防盜保護是否開啟等信息,手機防盜界面LostFindActivity.java 的效果圖如圖3-5 所示。
如果我們將Android.mk 中的目標文件名:LOCAL_MODULE := hello 寫成了LOCAL_MODULE:=hello.so,那么當我們使用ndk 進行編譯的時候會出現如下錯誤。

jni

上面的異常告訴我們,LOCAL_MODULE 不能有文件拓展名。

2.4 源文件名寫錯

如果我們將Android.mk 中的源文件名:LOCAL_SRC_FILES := hello.c 寫成了LOCAL_SRC_FILES :=helo.c(少些了一個單詞),那么當使用ndk 進行編譯的時候會出現如下錯誤:

jni

2.5 平臺使用錯誤

如果我們編譯的時候用的是arm 平臺,但是將目標文件運行在了x86 平臺上,那么會產生如下錯誤。

jni

如果想讓我們編譯的動態庫既支持arm 平臺又支持x86 平臺,那么我們可以在工程中的jni 目錄下添加Application.mk 文件。關于Application.mk 的配置在本人的上一個文檔中有說明,這里就不再介紹。

2.6 C 語言中被Java 調用的方法名寫錯

比如C 語言中的方法jstring Java_com_itheima_jnihello_MainActivity_helloC 把helloC 寫成了heloC,那么將會報如下錯誤。

jni

3. 自動生成JNI 頭文件

對于一些特殊的Java 方法名,我們很難寫出其對應的JNI 方法名,比如:

public native String a_b__c_d();

這時候我們可以通過我們的JDK 工具自動生成頭文件。該工具位于JDK 中,如果我們給電腦配置JAVA_HOME 則可以直接在命令行中使用,使用法則很簡單。如下圖所示:

jni

下面演示如何使用javah 工具生成我們MainActivity.java 中native 方法的頭文件。
將命令行控制臺切換到我們MainActivity.class(注意:是.class 不是.java)所在目錄。本人的目錄如下:

jni

執行javah 命令:javah com.itheima.jnihello.MainActivity,發現報了如下錯誤:

jni

Tips:出現上面的錯誤時因為我用的是jdk7 版本,jdk7 在編譯MainActivity.class 類的時候會查找其所有的父類,但是在當前命令行中是不可能有MainActivity 類的父類路徑的。有兩種辦法可以解決上述問題:
1)改成jdk6 版本
2)將native 方法定義在另外一個獨立的類中
本人在這里采用第二種方法來解決該問題,因此我創建一個類,將所有的native 方法都定義在該類中。

jni

JNIMethod.java 代碼清單如下:

package com.itheima.jnihello.jni;
public class JNIMethod {
public native String a_b__c_d();
}

再次執行javah 命令:javah com.itheima.jnihello.jni.JNIMethod

jni

這次發現沒有報錯,生成的頭文件在如下位置:

jni

打開生成的頭文件,將生成的方法簽名拷貝出來添加到我們的hello.c 源文件中即可。

jni

4. JNI 的值傳遞

本章我們通過一個案例來介紹幾種常見的JNI 值傳遞場景。
創建一個新的Android 工程《JNI 值傳遞》。在工程中創建com.itheima.jnipassdata.DataProvider 類。在該類中定義三個native 方法。代碼清單如下:

public class DataProvider {
    /**
     * 模擬用C 語言計算一些負責算法
     * @param x
     * @param y
     * @return
     */
    public native int add(int x,int y);
    /**
     * 模擬用C 語言加密字符串
     * @param str
     * @return
     */
    public native String encideString(String str);
    /**
     * 模擬用C 語言進行一些圖像算法
     * @param colorArray
     * @return
     */
    public native int[] changeColor(int[] colorArray);
}

使用javah 工具生成頭文件

jni

生成的頭文件在相對于命令行的當前目錄下。
在工程中創建jni 目錄,在改jni 目錄中創建Hello.c 文件,將上一步生成的方法拷貝到Hello.c文件中,并實現里面的方法Hello.c 代碼清單如下。

#include <stdio.h>
#include <malloc.h>
#include "jni.h"
//把java 的string 轉化成c 的字符串
char* Jstring2CStr(JNIEnv* env, jstring jstr)
{
    char* rtn = NULL;
    jclass clsstring = (*env)->FindClass(env,"java/lang/String"); //String
    jstring strencode = (*env)->NewStringUTF(env,"GB2312"); //"gb2312"
    jmethodID mid = (*env)->GetMethodID(env,clsstring, "getBytes",
        "(Ljava/lang/String;)[B"); //getBytes(Str);
    jbyteArray barr= (jbyteArray)(*env)->CallObjectMethod(env,jstr,mid,strencode);
    // String .getByte("GB2312");
    jsize alen = (*env)->GetArrayLength(env,barr);
    jbyte* ba = (*env)->GetByteArrayElements(env,barr,JNI_FALSE);
    if(alen > 0)
    {
        rtn = (char*)malloc(alen+1); //"\0"
        memcpy(rtn,ba,alen);
        rtn[alen]=0;
    }
    (*env)->ReleaseByteArrayElements(env,barr,ba,0); //釋放內存空間
    return rtn;
}
jint Java_com_ithiema_jnipassdata_DataProvider_add
        (JNIEnv* env, jobject obj, jint x, jint y){
    return x+y;
};
jstring Java_com_ithiema_jnipassdata_DataProvider_encideString
        (JNIEnv * env, jobject obj, jstring jstr){
    char* str = Jstring2CStr(env,jstr);
    char* hello = "hello";
    strcat(str,hello);
    //下面的兩種寫法是一樣的效果
    //return (*(*env)).NewStringUTF(env,str);
    return (*env)->NewStringUTF(env,str);
};
jintArray Java_com_ithiema_jnipassdata_DataProvider_changeColor
        (JNIEnv * env, jobject obj, jintArray jarray){
    int size = (*env)->GetArrayLength(env,jarray);
    int* arr = (*env)->GetIntArrayElements(env,jarray,0);
    int i;
    for(i=0;i<size;i++){
        arr[i]+=10;
    }
    return jarray;
}

使用NDK 工具將上面的C 代碼編譯成動態庫文件,首先得在工程的jni 目錄下添加Android.mk和Application.mk 文件
在MainActivity 類中調用C 語言,MainActivity.java 代碼清單如下。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
              tools:context=".MainActivity" >

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/iv"
        />
    <Button
        android:layout_gravity="right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="lomo"
        android:onClick="click"
        />
</LinearLayout>

布局文件比較簡單,就不再給出。運行上面代碼,效果圖如下:

jni

4.1 讓C 語言輸出Log 日志

讓我們的C 語言也能在LogCat 中輸出一些信息是一個很簡單但也很常見的實際需求。為了方便演示我們直接在本章節中創建的工程中演示如何讓C 語言打印LogCat 日志。
在Android.mk 中添加如下屬性:LOCAL_LDLIBS := -llog,在C 源碼頭部引入如下頭文件和宏定義

#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

在C 代碼中可以將LOGD()或者LOGI()函數當做C 語言中的printf()函數使用。這里我將add 方法添加添加了一條日志輸入

 jint Java_com_ithiema_jnipassdata_DataProvider_add
 (JNIEnv* env, jobject obj, jint x, jint y){
 LOGI("x+y=%d",x+y);
 return x+y;
 }

重新編譯hello.so 動態庫,既然我們修改了Android.mk 和C 源碼文件,那么一定要記得重新使用NDK 編譯C 語言為動態庫,否則日志不會輸入而且也不報異常。
再次部署程序到模擬器,調用add 方法,發現在LogCat 中成功輸出了如下信息:

jni

5. 案例-調用美圖秀秀的動態庫

在美圖秀秀1.0 版本的時候程序員對其代碼并沒有加密以及反反編譯等處理,因此我們可以將其apk反編譯出來。里面關于圖形的核心算法都是通過so 庫來實現的,我們可以拿過來直接使用。
聲明:本文檔中使用的美圖秀秀只用于學習交流Android 技術,禁止用于其他目的!美圖秀秀1.0 版本下載地址
將下載好的mtxx.apk 進行反編譯,將反編譯好的資源作為原料備用。關于如何反編譯美圖秀秀請見本文檔最后一章。

Tips:解壓好的目錄打開lib 包,發現只有armeabi 一個文件夾,說明該so 文件只能在arm 架構的CPU上運行。
創建一個新的Android 工程《黑馬美圖秀秀》。將反編譯好的libmtimage-jni.so 拷貝到工程的libs->armeab(i 該目錄需要手動創建)目錄下。通過反編譯的 jar 包發現美圖秀秀的 jni 方法都定義在 JNI.java類中,我們用jd-gui 工具將反編譯的jar 包打開,找到JNI.java,然后拷貝其中的native 方法到我們的工程中。本人的工程目錄結構如下所示:

jni

Tips:我們使用了美圖秀秀的JNI 源碼,那么我們的JNI.java 名字以及其所在的包名必須嚴格跟原美圖秀秀保持一致,不然程序從so 動態庫中是找不到目標方法的。原因很簡單,因為C 中方法名是根據JNI.java 中的方法全限定名生成的。
編寫activity_main.xml 布局文件,布局文件清單如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
              tools:context=".MainActivity" >

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/iv"
        />
    <Button
        android:layout_gravity="right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="lomo"
        android:onClick="click"
        />
</LinearLayout>

在工程的res->drawable-hdpi 目錄下放入一張圖片。

jni

編寫MainActivity.java 代碼,在改類中實現核心方法

public class MainActivity extends Activity {
    private ImageView iv;
    private Bitmap bitmap;
    static{
        System.loadLibrary("mtimage-jni");
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.beautiful);
        iv = (ImageView) findViewById(R.id.iv);
        iv.setImageBitmap(bitmap);
    }
    public void click(View view){
        JNI jni = new JNI();
        //獲取Bitmap 的寬和高
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        //創建一個整型數組,存放每個像素點
        int[] pixels = new int[width*height];
        /**
         * 獲取bitmap 的像素數組
         * 第一個參數是int[]
         * 第二個參數是第獲取一個像素的偏移量,這里是0,也就是從第一個像素開始獲取
         * 第三個參數每行獲取多少個像素,當然bitmap 的寬度就是每行的像素個數
         * 第四個參數、第五個參數分別是開始獲取像素的x、y 坐標,這個坐標是相對于bitmap 本身來說的,因此是0,0
         * 第六、七個參數分別是每行、每列獲取像素的個數
         */
        bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
        //調用美圖秀秀API
        jni.StyleLomoB(pixels, width, height);
        //重新根據pixels 數組創建一個Bitmap 對象
        Bitmap newBitmap = Bitmap.createBitmap(pixels, width, height,
                bitmap.getConfig());
        iv.setImageBitmap(newBitmap);
    }
}

運行上面打代碼,效果如下:
1)處理前

jni18.png

2)處理后

jni19.png

6. C 語言調用Java 代碼

之前我們學習的JNI 都是Java 代碼調用C 代碼,本章節中演示C 代碼如何調用Java 代碼。
創建一個新的Android 工程《C 語言調用Java》,在工程中創建jni 目錄,在改目錄下放置Android.mk 和jni.h 文件(從老工程中拷貝)。工程目錄結構如下:

jni

編寫DataProvider.java 文件,在該文件中編寫native 方法,DataProvider.java 代碼清單如下:

public class DataProvider {
    //C 代碼調用該方法發送短信
    public void methodInJava(){
        System.out.println("我是Java 中的方法,我被調用了");
        SmsManager manager = SmsManager.getDefault();
        manager.sendTextMessage("5556", null, "hello I'm from Java", null, null);
    }
    //C 代碼調用該方法傳遞一個字符串,獲取一個字符串
    public String methodInJava2(String str){
        return "hello:"+str;
    }
    //C 代碼調用一個空參返回值為void 的實例方法
    public void methodInJava3(){
        System.out.println("我是Java 中的方法3,我被調用了");
    }
    //C 語言調用靜態方法
    public static void methodInJava4(){
        System.out.println("我是靜態方法。被調用了。");
    }
    //定義四個native 方法,這些方法在MainActivity 中被調用,這些方法在C 代碼中回調上面的Java 代碼
    public native void callCMethod();
    public native String callCMethod2(String str);
    public native void callCMethod3();
    public native void callCMethod4();
}

在jni 中創建calljava.c 文件,在該文件中實現調用java 的方法。代碼清單如下

#include <jni.h>
#include <stdio.h>
void Java_com_itheima_callJava_DataProvider_callCMethod(JNIEnv * env, jobject obj){
    //類似java 的反射,獲取java 對象
    jclass clazz = (*env)->FindClass(env,"com/itheima/callJava/DataProvider");
    //根據方法簽名獲取目標方法
    jmethodID methodID = (*env)->GetMethodID(env,clazz,"methodInJava","()V");
    //調用目標方法
    (*env)->CallVoidMethod(env,obj,methodID);
}
jstring Java_com_itheima_callJava_DataProvider_callCMethod2(JNIEnv * env, jobject
        obj,jstring str){
    jclass clazz = (*env)->FindClass(env,"com/itheima/callJava/DataProvider");
    jmethodID methodID =
            (*env)->GetMethodID(env,clazz,"methodInJava2","(Ljava/lang/String;)Ljava/lang/String;
            ");
            jstring jstr = (*env)->CallObjectMethod(env,obj,methodID,str);
    return jstr;
}
void Java_com_itheima_callJava_MainActivity_callCMethod3(JNIEnv * env, jobject obj){
    jclass clazz = (*env)->FindClass(env,"com/itheima/callJava/DataProvider");
    jmethodID methodID = (*env)->GetMethodID(env,clazz,"methodInJava3","()V");
    jobject o = (*env)->AllocObject(env,clazz);
    (*env)->CallVoidMethod(env,o,methodID);
}
void Java_com_itheima_callJava_DataProvider_callCMethod4(JNIEnv * env, jobject obj){
    jclass clazz = (*env)->FindClass(env,"com/itheima/callJava/DataProvider");
    jmethodID methodID = (*env)->GetStaticMethodID(env,clazz,"methodInJava4","()V");
    (*env)->CallStaticVoidMethod(env,clazz,methodID);
}

使用NDK 工具,將calljava.c 編譯成動態庫文件。(NDK 的使用在上一篇文檔中有詳細的介紹,這里就不再說明)
在MainActivity.java 中調用C 語言,代碼清單如下:

public class MainActivity extends Activity {
    static {
        System.loadLibrary("calljava");
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    public void click(View v) {
        new DataProvider().callCMethod();
    }
    public void click2(View view) {
        String result = new DataProvider().callCMethod2("ddd");
        Toast.makeText(this, result, 1).show();
    }
    public void click3(View v) {
        callCMethod3();
    }
    public void click4(View view) {
        new DataProvider().callCMethod4();
    }
    public native void callCMethod3();
}

布局文件比較簡單這里就不再給出。

7. 短信接收廣播之鎖屏用C++實現JNI

C++語言是面向對象的編程語言,源于C 語言,部分語法通用。本章節中主要介紹如何使用C++完成
簡單的JNI 開發。
創建一個新Android 工程《cpp 實現jni》,創建jni 包,在該包下創建JNI.java 文件,在該類中寫naive方法。因為我們是C++項目,因此需要給當前工程添加native Support。右鍵點擊項目,在彈出的對話框中選擇Android Tools,然后選擇Add native Support,彈出如下對話框:

jni

這個名字是C++源文件名,因為沒有實際的業務意義一次我們這里就隨便輸入一個了。
在工程中創建com.itheima.cppjni.jni 包,在該包下創建JNI.java,JNI.java 代碼清單如下:

public class JNI {
public native void helloFormCPP();
}

使用javah 工具生成頭文件,并將生成的頭文件拷貝到工程根目錄下的jni 目錄中。

jni

為了讓編譯器提示C++語言,我們需要給工程添加C++庫。
右鍵點擊工程,選擇Properties,在彈出的對話框中選擇C/C++ General->Paths And Symbols,彈出如下對話框:

jni

點擊圖中紅色框中的Add 按鈕,在彈出的對話框(如下圖)中點擊File system,然后選擇ndk 安裝目錄,選擇如下目錄:D:\software\ndkr9\android-ndk-r9b\platforms\android-19\arch-arm\usr\include

jni24.png

然后點擊OK。

Tips:上面目錄中紅色的是本人的ndk 所在目錄,大家找到自己的ndk 所在目錄即可。然后隨便選擇一個platform 即可,但是推薦大家選擇一個高版本的平臺。在gaga.cpp 中引入上面生成的頭文件。同時編輯gaga.cpp,代碼清單如下:

#include <jni.h>
#include "com_itheima_cppjni_jni_JNI.h"
JNIEXPORT jstring JNICALL Java_com_itheima_cppjni_jni_JNI_helloFromCPP
(JNIEnv * env, jobject obj){
return env->NewStringUTF("gaga from cpp");
};

編寫MainActivity.java,在該方法中加載動態庫

public class MainActivity extends Activity {
    static{
        System.loadLibrary("gaga");
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    public void click(View view){
        JNI jni = new JNI();
        String helloFromCPP = jni.helloFromCPP();
        Toast.makeText(this, helloFromCPP, 1).show();
    }
}

將上面工程部署到一個arm 架構的模擬器上。在部署的時候觀察控制臺,發現控制臺輸出如下信息

**** Build of configuration Default for project cpp 代碼jni 開發****
D:\software\ndkr9\android-ndk-r9b\ndk-build.cmd all
C:\Users\thinkpad\workspace\cpp 代碼jni 開發>rem This is a Windows cmd.exe script used to
invoke the NDK-specific GNU Make executable
C:\Users\thinkpad\workspace\cpp 代碼jni 開發>call
"D:\software\ndkr9\android-ndk-r9b\find-win-host.cmd" NDK_WIN_HOST
Android NDK: WARNING: APP_PLATFORM android-19 is larger than android:minSdkVersion 8
in ./AndroidManifest.xml
[armeabi] Compile++ thumb: gaga <= gaga.cpp
 [armeabi] StaticLibrary : libstdc++.a
 [armeabi] SharedLibrary : libgaga.so
 [armeabi] Install : libgaga.so => libs/armeabi/libgaga.so
 **** Build Finished ****

Tips:通過控制臺,我們發現當我們的工程添加本地支持以后,當我們在部署的時候eclipse 會自動的完成動態庫的編譯工作。而且我們還發現在生成動態之前先生成了libstdc++.a 靜態庫然后才生成了動態庫。上面代碼運行效果如下圖所示:

jni

8. 案例-鍋爐壓力監測

需求:硬件設備可以監測鍋爐的壓力,監測代碼邏輯是用C 語言編寫。客戶端用java 代碼每一秒調用一次C 語言,以獲取鍋爐的壓力值,然后將鍋爐的壓力值以動態柱形圖的形式顯示在手機客戶端。

Tips:分析上面的需求,我們需要使用jni 技術讓Java 和C 代碼通信。在C 語言端我們可以調用隨機函數模擬鍋爐壓力的動態變化。在Java 端,我們可以自定義一個View 對象顯示我們的鍋爐壓力。
創建一個Android 工程《JNI 鍋爐壓力檢測》,在該工程中創建jni 目錄,將Android.mk、jni.h 從其他工程中拷貝到該目錄下。
在jni 目錄下創建一個C 源文件pressure.c,代碼清單如下:

#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
//定義一個返回值為int 型的函數,返回一個100 以內的函數值rand()是C 語言的一個隨機函數
int getPressure(){
return rand()%100;
}
jint Java_com_itheima_jniPressure_MainActivity_getPressure(){
return getPressure();
 }

將pressure.c 編譯成動態庫文件,在MainActivity 的同一個包目錄下創建一個自定義控件類MyView 繼承View 類。

public class MyView extends View {
    int top = 100;
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    Paint paint = new Paint();
    public MyView(Context context) {
        super(context);
        paint.setColor(Color.RED);
        paint.setTextSize(18);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        /**
         * 繪制一個矩形區域
         * 第一、二、三、四個參數分別代表繪圖區域離畫布左、上、右、下的距離。這四個參數確定了矩形的大小也確定了
         */
        canvas.drawRect(60, top, 90, 150, paint);
        canvas.drawText("當前壓力值:"+(150-top),60, 170, paint );
        super.onDraw(canvas);
    }
    //根據壓力轉換成繪圖區域的top 值
    public void setPressure(int pressure){
        top = 150-pressure;
        if (pressure<30) {
            paint.setColor(Color.GREEN);
        }else if (pressure<60) {
            paint.setColor(Color.YELLOW);
        }else {
            paint.setColor(Color.RED);
        }
    }
}

編寫MainActivity.java 代碼,在該代碼中實現Java 端的核心方法。

public class MainActivity extends Activity {
    //加載動態庫
    static {
        System.loadLibrary("pressure");
    }
    //聲明一個Timer 和TimerTask,用于定時任務的處理
    private Timer     timer;
    private TimerTask task;
    private MyView    view;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //創建自定義控件對象
        view = new MyView(this);
        //將自定義控件對象作為顯示對象,而沒有使用布局文件
        setContentView(view);
        //創建定時任務
        timer = new Timer();
        task = new TimerTask() {
            @Override
            public void run() {
                view.setPressure(getPressure());
                //通過子線程通知控件重繪
                view.postInvalidate();
            }
        };
        //每500 毫秒執行一次,第二個參數是延時執行,這里為0 也就是第一次會立即執行
        timer.schedule(task, 0, 500);
    }
    //調用native 方法,獲取壓力值
    public native int getPressure();
}

將上面工程部署到arm 架構的模擬器上。運行結果如下圖所示。

jni

9. 案例-監聽應用程序的卸載

需求:當我們的apk 安裝在Android 手機上后,我們可以在其后臺開啟個C 語言編寫的死循環,C 語言編程的程序跟我們的應用不在同一個進程中,因此當我們的應用軟件被卸載的時候,C 語言可以監測到。
監測原理就是訪問/data/data/{包名}文件是否存在,如果不存在顯然是被刪除了。

Tips:考慮到我們上面的案例跟這個案例使用的知識點差不多,都是使用Java 語言調用C 語言。因此這里就不再一步一步的演示如何創建工程。這里只給出核心的C 語言代碼。本人工程目錄結構如下所示:

jni

這里只給出listen.c 源文件清單,核心方法是這個文件實現的。在MainActivity.java 中加載該動態庫,并在onCreate 方法中調用我們的C 語言函數即可。listen.c 代碼清單如下所示:

#include <jni.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <android/log.h>
#define LOG_TAG "System.out"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
void Java_com_itheima_jniuninstall_MainActivity_listen(
        JNIEnv * env, jobject obj) {
    pid_t pid = fork(); //叉子
    if (pid == 0) { //當前是分叉出來的進程。
        int isStop = 1;
        while (isStop) {
            //監視當前應用程序的包名文件夾是否存在如果不存在了就是被卸載了。
            FILE* file; //文件的指針
            file = fopen("/data/data/com.itheima.jniuninstall", "r");
            if (file == NULL) {
                //被卸載了。
                LOGI("uninstalled\n");
                //開啟一個網頁了。
                // int execlp(char *pathname, char *arg0, arg1, .., NULL);
                execlp("am","am","start","-a","android.intent.action.VIEW","-d","http://www.baid.
                        com",NULL);
                        isStop = 0;
            } else {
                //沒有被卸載
                LOGI("haha huode henhao \n");
            }
            //讓線程休眠2 秒在C 語言中是秒為單位的不是毫秒
            sleep(2);
        }
    }
}

Tips:上面的execlp 函數用于調用本地系統(Android 系統)命令打開一個瀏覽器,訪問一個指定的URL。但是經過我的測試發現在低版本模擬器(2.3)上該功能可是使用但是在高版本模擬器(4.3)上不可以使用。

10. 安卓逆向助手

有一款叫安卓逆向助手軟件反編譯apk 十分方便。這里給大家介紹的反編譯方法就是基于這款軟件的。安卓逆向助手下載地址

Android逆向助手是一功能強大的逆向輔助軟件。該軟件可以幫助用戶來進行apk反編譯打包簽名;dex/jar互轉替換提取修復;so反編譯;xml、txt加密;字符串編碼等等,操作簡單,只需要直接將文件拖放到源和目標文件。

安卓逆向助手

將下載好的rar 包解壓縮以后目錄結構如下(內置的廣告被我刪除后的)

Tips:lib 目錄存放都是用java 寫的核心反編譯邏輯,必須跟exe 文件放在同一個目錄下。

打開Android 逆向助手.exe,如下圖所示:

安卓逆向助手

選擇源文件,并且選擇(也是默認的選擇)反編譯apk,我們找到mtxx.apk 的路徑,然后點擊操作。
在mtxx.apk 目錄下生成了一個mtxx 文件夾,打開該文件,目錄結構如下圖所示:

安卓逆向助手

在上面操作后打開lib 目錄可以找到美圖秀秀的動態庫文件,但是我們還需要找到其java 代碼。顯然
美圖秀秀用smali 算法反編譯了。那么我們接著下一步。

安卓逆向助手

在Android 逆向助手.exe 中打開源文件,選擇提取dex 點擊執行。

安卓逆向助手

這時候在目標文件夾下生成了dex 文件

安卓逆向助手

最后在Android 逆向助手.exe 中選擇dex 轉jar 選項。在源文件中選擇上一步生成的classes.dex 文件,然后點擊執行(這個過程大概需要幾秒的等待時間)。這時候該軟件會自動將我們生成的jar 文件用jd-gui工具打開。打開效果如下所示:

安卓逆向助手

11. jadx

jadx是新一代反編譯大殺器,github地址。Android開發(/學習)有時候需要用到反編譯工具,Window上有很多工具,而Mac上則不多,這里稍微介紹一下Mac上可用的反編譯工具Jadx.

11.1 準備

clone 倉庫,編譯

mkdir jadx
git clone https://github.com/skylot/jadx.git
cd jadx
./gradlew dist #這個需要稍微等待一下

開始反編譯,等完畢后,可以開始了,我就介紹個最簡單最常用的用法

把apk改成zip,解壓zip獲取class.dex文件,將class.dex文件放到jadx目錄下

cd build/jadx/
bin/jadx -d out class.dex  # 反編譯后放入out文件夾下(如果out不存在它會自動創建)
#or
bin/jadx-gui class.dex  # 會反編譯,并且使用gui打開
jadx

OK,就這樣,后續還可以配置環境變量,更加方便

更多Android反編譯工具

12.1 Classyshark

輕松查看apk內部每個包的方法數,用了哪些開源庫,同樣拿知乎開刀做例子


12.2 smalidea

smali代碼調試插件,你以為沒有拿到安卓Java源碼就不能調試了嗎?圖樣圖森破了吧


12.3IDA Pro

IDA Pro,逆向大利器,不管你是smali還是so文件,照樣動態調試你

12.4 Android Killer

下載地址1 下載地址2 使用指南
集Apk反編譯、Apk打包、Apk簽名,編碼互轉, ADB通信(應用安裝-卸載-運行-設備文件管理)等特色功能于一 身,支持logcat日志輸出,語法高亮, 基于關鍵字(支持單行代碼或多行代碼段)項目內搜索, 可自定義外部工具;吸收融匯多種工具功能與特點, 打造一站 式逆向工具操作體驗,大大簡化了用戶在 安卓應用/游戲修改過程中的各類繁瑣工作。

12.5 SmaliViewer

下載地址 使用指南
是一款免費的APK分析軟件,無論從分析的深度還是廣度來看,都是一款能夠滿足用戶需求的產品,使您在APK分析的過程中,更加得心應手。

12.6 Enjarify

Enjarify 是一個用 Python 寫的, Google 官方開源的可以將 Dalvik 字節碼轉換為 Java 字節碼的工具。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,536評論 25 708
  • JNI開發系列閱讀 JNI與底層調用1 JNI與底層調用2 C/C++在Android開發中的應用 1. JNI ...
    JackChen1024閱讀 737評論 0 3
  • _ 聲明: 對原文格式以及內容做了細微的修改和美化, 主要為了方便閱讀和理解 _ 一. 基礎 Java Nativ...
    元亨利貞o閱讀 6,003評論 0 34
  • 如果有下輩子,我想做一個毛絨娃娃,不要長的太好看,不奢求有太多的人喜歡,我只想有一個真正喜歡我的小伙伴,他特別喜歡...
    桔子v閱讀 103評論 0 1
  • 如何量化,用數學語言去表述信息(information)這個概念?這似乎是一個很難的問題,因為信息所包含的概念太廣...
    Oo_閱讀 810評論 0 0