android开发艺术(十一)之 综合技术 and JNI和NDK开发

1.使用CrashHandler来获取应用的crash信息

解决方法:
Thread的setDefaultUncaughtExceptionHandler
当crash 发生的时候,系统就会回调UncaughtExceptionHandler的uncaughtException方法,在uncaughtException方法中就可以获取到异常信息,可以选择把异常信息存储到SD卡中,然后在合适的时机通过网络将crash信息上传到服务器上
具体实现:
首先需要实现一个UncaughtExceptionHandler对象,在它的uncaughtException方法中获取异常信息并将其存储在SD卡中或者上传到服务器,然后调用Thread 的setDefaultUncaughtExceptionHandler方法将它设置为线程默认的异常处理器,由于默认异常处理器是Thread类的静态成员,因此它的作用对象是当前进程的所有线程。
当应用崩溃时,CrashHandler会将异常信息以及设备信息写入SD卡,如果系统有默认的异常处理机制,将异常交给系统处理,系统会终止程序,否则自行终止
具体使用CrashHandler

public class TestApp extends Application {
	private static TestApp sInstance;
	@Override
	public void onCreate() {
		super.onCreate() ;
		sInstance = this;
		//在这里为应用设置异常处理,然后程序才能获取未处理的异常
		CrashHandler crashHandler = CrashHandler.getInstance() ;
		crashHandler.init(this) ;
	}
	public static TestApp getInstance() {
		return sInstance ;
	}
}

2.解决方法数越界

android中存在一个限制,单个dex文件(包含android的FrameWork、依赖的jar包以及应用中的代码)的方法数不能超过65536,会出现编译错误:DexIndexOverflowException
解决方案1:google提供的multidex通过将一个dex文件拆分成多个dex文件来避免单个文件方法越界问题
解决方案2:动态加载,直接加载一个dex形式的文件。将部分代码打包到一个dex文件,并在程序运行时根据需要去动态加载该dex文件中的类

2.1 multidex

  1. app目录下的build.gradle文件添加配置项和添加依赖(这是android低版本添加依赖方法)
android {
    compileSdkVersion 28
    buildToolsVersion ...
    defaultConfig {
        // Enabling multidex support.
        multiDexEnabled true
    }
    ...
}

dependencies {
  compile 'com.android.support:multidex:1.0.0'
}
  1. 使用multidex
    2.1在manifest文件中指定Application为MultiDexApplication
<application
	android: name=" android.support.multidex.MultiDexApplication"
	android :allowBackup="true"
	android: icon="@mipmap/ic_ launcher"
	android: label="@string/app name”
	android: theme="@style/AppTheme">
</application>

2.2 让应用的Application继承MultiDexApplication

public class TestApplication extends MultiDexApplication {...}

2.3 重写Application的attachBaseContext方法,这个方法比Application的onCreate要先执行

public class TestApplication extends Application {
	@Override
	protected void attachBaseContext (Context base) {
		super .at tachBaseContext (base) ;
		MultiDex.install (this) ;
		}
}

3.如果需要指定主dex文件中所要包含的类
build.gradle文件中添加afterEvaluate区域,区域内部使用–main-dex-list指定所要包含的类

afterEvaluate {
	println "afterEvaluate"
	tasks.matching {
		it.name.startswith('dex')
	}.each{dx->
		def listFile = project.rootDir.absolutePath + ' /app/maindexlist. txt '
		println "root dir:" + project.rootDir.absolutePath
		println "dex task found:+ dx.name
		if (dx.addi tionalParameters == nul1) {
			dx.addi tionalParameters = []
		}
		dx. additionalParameters +='-- multi-dex'
		dx. additionalParameters +='--main-dex-list=' + listFile
		dx. additionalParameters +='--minimal-main-dex'
	}
]

–multi-dex 表示当方法数越界时则生成多个dex 文件
–main-dex-list指定了要在主dex中打包的类的列表
–minimal-main-dex 表明只有–main- dex-list所指定的类才能打包到主dex中。
它的输入是一个文件,在上面的配置中,它的输入是工程中app目录下的maindexlist.txt 这个文件,在maindexlist.txt中则指定了一系列的类,所有在maindexlist.txt中的类都会被打包到主dex中。maindexlist.txt这个文件名是可以修改的,但是它的内容必须要遵守一定的格式。multidex 的jar包中的9个类必须也要打包到主dex中,否则程序运行时会抛出异常,告知无法找到multidex相关的类。这是因为Application 对象被创建以后会在attachBaseContext方法中通过MultiDex.install(this)来加载其他dex文件,这个时候MultiDex相关的类必须在主dex中,同时由于Application的成员和代码块会先于attachBaseContext方法而初始化,而这个时候其他dex文件还没有被加载,因此不能在Application的成员以及代码块中访问其他dex中的类
在这里插入图片描述
局限性:
(1)应用启动速度会降低。由于应用启动时会加载额外的dex文件,这将导致应用的启动速度降低,甚至可能出现ANR现象,尤其是其他dex文件较大的时候,因此要避免生成较大的dex文件。
(2)由于Dalvik linearAlloc的bug,这可能导致使用multidex的应用无法在Android 4.0以前的手机上运行,因此需要做大量的兼容性测试。同时有可能出现应用在运行中由于采用multidex方案从而产生大量的内存消耗的情况,这会导致应用崩溃。

2.2 android的动态加载

动态加载技术实现的开源插件化框架DL
宿主是指普通的apk,而插件一般是指经过处理的dex或者apk,在主流的插件化框架中多采用经过特殊处理的apk来作为插件,很多插件化框架都需要用到代理Activity的概念,插件Activity的启动大多数是借助一个代理Activity来实现的。

1. 资源访问

Activity 的工作主要是通过ContextImpl来完成的,Context中有两个抽象方法,通过它们来获取资源。这两个抽象方法的真正实现在Contextlmpl中。

//Return an AssetManager instance for your application's package
public abstract AssetManager getAssets() ;
//Return a Resources instance for your application's package.
public abstract Resources getResources() ;

具体实现:

  • 首先调用loadResources通过反射加载apk中的资源,通过addAssetPath方法将apk的资源加载到Resources对象中
  • 然后在代理Activity中实现getAssets()方法和getResourcces()方法
  • 最后可以通过R来访问插件中资源

2. Activity生命周期的管理

  1. 反射方式
    首先通过Java的反射去获取Activity的各种生命周期方法,例如onCreate等;然后在代理Activity中去调用插件Activity对应的生命周期方法
    缺点:
    一方面是反射代码写起来比较复杂,另一方面是过多使用反射会有一定的性能开销。
  2. 接口方式
    将Activity 的生命周期方法提取出来作为一个接口(比如叫DLPlugin), 然后通过代理Activity去调用插件Activity的生命周期方法
public interface DLPlugin {
public void onStart() ;
public void onRestart() ;
...

调用上述方法,mRemoteActivity就是DLPlugin接口的实现:
@Override 
protected void onStart (){
	mRemoteActivity.onStart() ;
	super.onStart() ;
}
@override
protected void onRestart() {
	mRemoteActivity.onRestart() ;
	superonRestart() ;
}
...

3. ClassLoader的管理

为了更好地对多插件进行支持,需要合理地去管理各个插件的DexClassoader,这样同一个插件就可以采用同一个ClassLoader 去加载类,从而避免了多个ClassLoader加载同一个类时所引发的类型转换错误。通过将不同插件的ClassLoader存储在一个HashMap中,保证不同插件的类互不干扰。

3.反编译

  1. dex2jar
    可以将一个apk转成一个jar包,在通过反编译工具jd-gui打开就可以看到源码
  2. Apktool
    主要用于应用的解包和二次打包
    下载地址:
    apktool
    dex2jar
    jd-gui

3.1 使用dex2jar和jd-gui反编译apk
Dex2jar 是一个将dex文件转换为jar包的工具,dex 文件来源于apk,将apk通过zip包的方式解压缩即可提取出里面的dex文件。因为jar包中都是class文件,这个时候还需要jd-gui 将jar包进一步转换为Java代码,dex2jar和jd-gui在不同的操作系统中的使用方式都是一致的。
Dex2jar是命令行工具,它的使用方式如下:
Linux (Ubuntu):./dex2jar.sh classes. dex
Windows: dex2jar.bat classes.dex
Jd-gui是图形化工具,直接双击打开后通过菜单打开jar包即可查看jar包的源码。

3.2 使用apktool对apk进行二次打包

  • 反编译出apk中的二进制数据资源
  • 二次打包

apktool是一个命令行工具:
二次打包完需要经过签名之后才能安装
Linux ( Ubuntu)
解包: ./apktool d -f CrashTestapk CrashTest
二次打包: ./apktool b CrashTest CrashTest-fake.apk
签名:java -jar signapk.jar testkey.x509.pem testkey.pk8 CrashTest-fake.apk CrashTest-fake-signed.apk
Windows
解包: apkool.bat d -f CrashTestapk CrashTest
二次打包: apktool.bat b CrashTest CrashTest-fake.apk
签名:java -jar signapk.jar testkey.x509.pem testkey.pk8 CrashTest-fake apk CrashTest-fake-signed.apk

4.JNI和NDK编程

JNI:用于和本地代码进行交互,可以调用c、c++所编写的本地代码,增强Java语言的本地交互能力
NDK:Android所提供的一个工具集合,通过NDK可以在Android中更加方便地通过JNI来访问本地代码. NDK还提供了交叉编译器,只需要简修改mk文件就可以生成特定CPU平台的动态库(以.so为后缀的文件),使用NDK有如下好处:
(1)提高代码的安全性。由于so库反编译比较困难,因此NDK提高了Android程序的安全性。
(2)可以很方便地使用目前已有的C/C++开源库。
(3)便于平台间的移植。通过C/C++实现的动态库可以很方便地在其他平台上使用。
(4)提高程序在某些特定情形下的执行效率,但是并不能明显提升Android程序的性能。

4.1 JNI的开发流程

首先在JAVA中声明native方法,然后用c或c++实现native方法,最后编译运行
1.在Java中声明native方法
加载动态库:System.loadLibrary("jni-test")
jni-test是so库的标识,完整名称是libjni-test.so

package com.example.application;
import java.lang.System;
public class JniTest {
	static{
		System. loadLibrary("jni-test") ; }
	public static void main(String args []) {
		JniTest jniTest = new JniTest () ;
		System.out.println(jniTest.get()) ;
		jniTest.set("hello world") ;
		}
	public native String get() ;
	public native void set(String str) ;

2.编译Java源文件得到class文件,然后通过javah命令导出JNI的头文件

javac com/ryg/JniTest.java
javah com.ryg.JniTest

导出的头文件com_ ryg_ JniTest.h中有几点需要注意:

  • JNIEXPORT、JNICALL、JNIEnv和jobject 都是JNI标准中所定义的类型或者宏,它们的含义如下:
    ●JNIEnv*:表示一个指向JNI环境的指针,可以通过它来访问JNI提供的接口方法;
    ●jobject:表示Java对象中的this;.
    ●JNIEXPORT和JNICALL:它们是JNI中所定义的宏,可以在jni.h这个头文件中查找到。
  • 头文件中存在的宏定义是必须的

3.实现JNI方法
JNI方法是指Java中声明的native方法,这里可以选择用C++或者C来实现,分别用C++和C来实现JNI方法。首先,在工程的主目录下创建一个子目录,名称随意,这里选择jni作为子目录的名称,然后将之前通过javah生成的头文件com_ ryg_ JniTest.h 复制到jni目录下,接着创建test.cpp和test.c两个文件,不同点在两者的env操作上:

// test. cpp
#include "com_ryg_JniTest.h"
#include <stdio.h>
JNIEXPORT jstring JNICALL Java com ryg_ JniTest get (JNIEnv *env, jobject thiz)
	printf("invoke get in c++\n") ;
	return env->NewStringUTF ("He11o from JNI !") ;  }
JNIEXPORT void JNICALL Java com ryg_ JniTest_ set (JNIEnV *env, jobject thiz,jstring string) {
	printf("invoke set from C++\n") ;
	char* str = (char*)env->GetStringUTFChars (string, NULL) ;
	printf ("号s\n", str) ;
	env->ReleaseStringUTFChars (string, str) ;  }
// test.c
#include "com_ryg_JniTest .h"
#include <stdio.h>
JNIEXPORT jstring JNICALL Java_ .com_ ryg_ JniTest_ get (JNIEnv *env, jobject thiz)
	printf ("invoke get from C\n") ;
	return (*env) ->NewStringUTF (env, "Hello from JNI !") ;  }
JNIEXPORT void JNICALL Java_ com ryg_ JniTest_ set (JNIEnv *env, jobject thiz, jstring string)
	printf ("invoke set from C\n") ;
	char* str = (char*) (*env)->GetStringUTFChars (env, string , NULL) ;
	printf("gs\n", str) ;
	(*env) ->ReleaseStringUTFCharsenv, string, str); }

4.编译so库并在Java中调用
so库的编译这里采用gcc,切换到jni目录中,编译指令如下所示。
C++:gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC test.cpp -o libjni-test.so
C:gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC test.c -o libjni-test.so
上面的编译命令中,/usrlibijvm/java-7-openjdk-amd64是本地的jdk的安装路径,在其他环境编译时将其指向本机的jdk路径即可。而libjni-test.so则是生成的so 库的名字。so 库编译完成后,最后通过Java指令来执行Java 程序,切换到主目录,执行如下指令,其中
-Djava.library.path=jni 指明了so库的路径。

java -Djava.library.path=jnicom.ryg.JniTest

4.2 NDK的开发流程

1.下载并配置NDK
首先要从Android 官网上下载NDK,下 载地址为htps:/veloerandroid. com/ndk/
downloads/index.html,本文中采用的NDK的版本是android-ndk-r10d。下载完成以后,将NDK解压到一个目录,然后为NDK配置环境变量,步骤如下

  1. 首先打开当前用户的环境变量配置文件:vim ~/.bashrc
  2. 然后在文件后面添加如下信息: export PATH=~/Android/android-ndk-r10d:SPATH,其中~/Android/android-ndk-r10d是本地的NDK的存放路径。
  3. 添加完毕后,执行source ~/.bashrc来立刻刷新刚刚设置的环境变量。
  4. 设置完环境变量后,ndk-build 命令就可以使用了,通过ndk-build命令就可以编译产生so库。

2.创建一个android项目,并在主活动中声明所需要的native方法
3.实现android项目中所声明的方法
在外部创建一个名为jni的目录,然后该目录下创建三个文件,test.cpp(实现native方法),Android.mk,Application.mk
4.切换到jni目录的父目录,然后通过ndk-build命令编译产生so库
这个时候NDK会创建一个和jni目录平级的目录libs,libs下面存放的就是so库的目
录,需要注意的是,ndk-build 命令会默认指定jni目录为本地源码的目录,如果源码存放的目录名不是jni,那么ndk-build则无法成功编译。
5.最后移动到androidstudio运行
最后在app/src/main中创建一个名为jniLibs的目录,将生成的so库复制到jniLibs目录中,然后通过AndroidStudio编译运行即可
需要注意的是,可以不使用默认识别目录jniLibs,通过修改App的build.gradle实现so库存放目录的修改;可以不通过ndk-build手动创建,通过App的build.gradle的defaultConfig添加NDK选项,制定AS自动生成的so库文件名。

4.3 JNI的数据类型和类型签名

JNI数据类型包含两种,基本类型和引用类型(类、对象和数组),需要知道:

  1. JNI基本数据类型和Java的对应关系
  2. 引用类型和Java的对应关系

JNI的类型签名标识了一个特定的Java类型,这个类型既可以是类和方法,也可以是数据类型

  • 基本数据类型的签名采用首字母大写来表示,特殊的boolean使用Z表示
  • 类的签名采用“L+包名+类名+;”的形式,只需要将其中的.替换为即可。比如java.lang.String,它的签名为Ljava/lang/String;,注意末尾的;也是签名的一部分。
  • 对象的签名是对象所属的类的签名,String对象的签名是Ljava/lang/String;
  • 数组的签名为**[+数据类型签名**,多维数组的签名是维数×[+数据类型签名
  • 方法的签名是**(参数类型签名)+返回值类型签名**,例如
    boolean fun(int a,String b,double []c)的签名是(ILjava/lang/String;[D)Z

4.4 JNI调用Java方法的流程

JNI调用Java方法的流程是先通过类名找到类,然后再根据方法名找到方法的id,最后就可以调用这个方法了。如果是调用Java中的非静态方法,那么需要构造出类的对象后才能调用它。
首先在Java中定义一个方法供JNI调用:

publ1c static void methodcalledByJni(String msgFromJni){
	Log.d (TAG, "nethodcalledByJni,msg:+ ngFromJnl);}

然后在JNI中调用该方法

vold callJavaMethod(JNIEnv*tenv, jobject thiz) (
//先根据类名找到类
	jclass clazz - env->FindClas ("com/ ryg/JniTestApp/MainActivity");
	if (clazz ==NULL){
		printf("find class MainActivity error!");
		return;  }
		//再根据方法名找到方法methodCalledByJni
	jmethodID 1d.env->GetStaticMethodID(clazz, "methodCalledByJni""(Ljava/lang/string;)V");//
	if(id == NULL) {
		printf("find method methodcalledByJni error!"); }
	jstring msg m env->NewStringUTF ("msg send by cal1JavaMethod in test.cpp.") ;
	//完成最终调用
	env->CallStaticVoidMethod(clazz, id, msg) :

最后调用callJavaMethod方法

jstring Java_com_ry9_JniTestApp_MainActivity_get(JNIEnv *env,jobject thiz){
	printf("invoke get in C++\n") :
	callJavaMethod(env, thiz);
	return env->NewStringUTF("Hello from JNI in libjni-test.so !"):

总的来说就是 MainActivity调用JNI 的Java_com_ry9_JniTestApp_MainActivity_get方法,然后该方法调用callJavaMethod方法,然后该方法接着调用在java中定义的methodCalledByJni方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值