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
- app目录下的build.gradle文件添加配置项和添加依赖(这是android低版本添加依赖方法)
android {
compileSdkVersion 28
buildToolsVersion ...
defaultConfig {
// Enabling multidex support.
multiDexEnabled true
}
...
}
dependencies {
compile 'com.android.support:multidex:1.0.0'
}
- 使用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生命周期的管理
- 反射方式
首先通过Java的反射去获取Activity的各种生命周期方法,例如onCreate等;然后在代理Activity中去调用插件Activity对应的生命周期方法
缺点:
一方面是反射代码写起来比较复杂,另一方面是过多使用反射会有一定的性能开销。 - 接口方式
将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() ;
super。onRestart() ;
}
...
3. ClassLoader的管理
为了更好地对多插件进行支持,需要合理地去管理各个插件的DexClassoader,这样同一个插件就可以采用同一个ClassLoader 去加载类,从而避免了多个ClassLoader加载同一个类时所引发的类型转换错误。通过将不同插件的ClassLoader存储在一个HashMap中,保证不同插件的类互不干扰。
3.反编译
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配置环境变量,步骤如下
- 首先打开当前用户的环境变量配置文件:
vim ~/.bashrc
- 然后在文件后面添加如下信息:
export PATH=~/Android/android-ndk-r10d:SPATH,
其中~/Android/android-ndk-r10d是本地的NDK的存放路径。 - 添加完毕后,执行
source ~/.bashrc
来立刻刷新刚刚设置的环境变量。 - 设置完环境变量后,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数据类型包含两种,基本类型和引用类型(类、对象和数组),需要知道:
- JNI基本数据类型和Java的对应关系
- 引用类型和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方法