第13章 综合技术
程序很难避免不crash,用户使用时crash,这个crash很难获取,我们通过CrashHandler来监视应用的crash信息,给程序设置CrashHandler,当程序崩溃时调用CrashHandler的unCaughtExecption方法,在这里获取Crash信息并上传到服务器上,进而实现崩溃监控。
Android中有一个限制是整个应用的方法总数不超过65536,否则会出现编译错误,为了解决该问题,可以采用multidex专门解决该问题,通过将一个dex文件拆分为多个dex文件来避免单个dex文件无法加载的问题。
方法数越界的解决方法除了multidex拆分之外,我们还有动态加载技术。按需进行加载。 在程序执行时动态加载dex中的类。
反编译:dex2jar和apktool。Dex2jar将一个apk转成一个jar包,利用jd-gui可以查看到反编译后的Java代码。
(1)使用CrashHandler来获取应用的crash信息
用户crash信息无法被捕捉,需要收集到相关的日志信息,安卓的Thread给我们提供了一个setDefaultUncaughtExceptionHandler。
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler){
Thread.defaultUncaughtHandler = handler;
}
当crash发生时,系统在崩溃的时候回调UncaughtExceptionHandler的uncaughtException方法就可以获取到异常信息,可以选择把异常信息存储到SD卡里,然后再合适的时机通过网络将crash信息上传到服务器,后续版本中修复。尽量温和退出,譬如弹框之类。
public class CrashHandler implements UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private static final boolean DEBUG = true;
private static final String PATH = Environment.getExternalStorageDirectory().getPath() + "/CrashTest/log/";
private static final String FILE_NAME = "crash";
private static final String FILE_NAME_SUFFIX = ".trace";
private static CrashHandler sInstance = new CrashHandler();
private UncaughtExceptionHandler mDefaultCrashHandler;
private Context mContext;
private CrashHandler() {
}
public static CrashHandler getInstance() {
return sInstance;
}
public void init(Context context) {
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
mContext = context.getApplicationContext();
}
/**
* 这个是最关键的函数,当程序中有未被捕获的异常,系统将会自动调用#uncaughtException方法
* thread为出现未捕获异常的线程,ex为未捕获的异常,有了这个ex,我们就可以得到异常信息。
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
try {
//导出异常信息到SD卡中
dumpExceptionToSDCard(ex);
uploadExceptionToServer();
//这里可以通过网络上传异常信息到服务器,便于开发人员分析日志从而解决bug
} catch (IOException e) {
e.printStackTrace();
}
ex.printStackTrace();
//如果系统提供了默认的异常处理器,则交给系统去结束我们的程序,否则就由我们自己结束自己
if (mDefaultCrashHandler != null) {
mDefaultCrashHandler.uncaughtException(thread, ex);
} else {
Process.killProcess(Process.myPid());
}
}
private void dumpExceptionToSDCard(Throwable ex) throws IOException {
//如果SD卡不存在或无法使用,则无法把异常信息写入SD卡
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
if (DEBUG) {
Log.w(TAG, "sdcard unmounted,skip dump exception");
return;
}
}
File dir = new File(PATH);
if (!dir.exists()) {
dir.mkdirs();
}
long current = System.currentTimeMillis();
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(current));
File file = new File(PATH + FILE_NAME + time + FILE_NAME_SUFFIX);
try {
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));
pw.println(time);
dumpPhoneInfo(pw);
pw.println();
ex.printStackTrace(pw);
pw.close();
} catch (Exception e) {
Log.e(TAG, "dump crash info failed");
}
}
private void dumpPhoneInfo(PrintWriter pw) throws NameNotFoundException {
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
pw.print("App Version: ");
pw.print(pi.versionName);
pw.print('_');
pw.println(pi.versionCode);
//android版本号
pw.print("OS Version: ");
pw.print(Build.VERSION.RELEASE);
pw.print("_");
pw.println(Build.VERSION.SDK_INT);
//手机制造商
pw.print("Vendor: ");
pw.println(Build.MANUFACTURER);
//手机型号
pw.print("Model: ");
pw.println(Build.MODEL);
//cpu架构
pw.print("CPU ABI: ");
pw.println(Build.CPU_ABI);
}
private void uploadExceptionToServer() {
//TODO Upload Exception Message To Your Web Server
}
}
(2)使用multidex来解决方法数越界
Android单个dex文件能够包含的最大总数是65536,达到后编译时抛出异常。解决方法:multi-dex方案。在5.0之前使用muitidex需要引用android-support-nultidex.jar包,SDK下可找到;5.0后默认支持multi-dex。
步骤一:在build.gradle中加入multidex的依赖。
multiDexEnabled true //defaultConfig中
compile 'com.android.support:multidex:1.0.0' //加入依赖包
步骤二:在代码中加入支持multidex的功能:方法一:在Manifest中指定Application为android.support.multidex.MultiDexApplication;方法二:让APP李cation继承自MultiDexApplication:public class MainActivity extends MultiDexApplication{....};方法三:重写Application的attachBaseContext方法,此方法比onCreate先执行。推荐第三种。
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
MultiDex.install(this);
}
也可以自定义build.gradle文件来定制dex文件的生成过程。Multidex可能带来的问题:(1)应用启动速度会降低,启动时加入额外dex文件,启动速度降低可能会出现ANR,(2)由于Dalvik linewralloc的bug,可能导致multidex的应用无法在4.0以前手机上使用。需要做兼容性测试。
(3)Android的动态加载技术
动态加载技术(插件化)很重要,当App越来越庞大,通过插件化来减轻应用内存和CPU占用。可以实现热插拔,即不发布新版本情况下更新某些模块。
不同插件各有特色,但必须解决三个基础问题:资源访问,Activity的生命周期和ClassLoader的管理。了解宿主和插件的概念。宿主是普通apk,插件是经过处理的apk或者dex。主流插件化框架中多采用经特殊处理的apk作为插件。插件Activity启动大多数是借助一个代理Activity来实现的。
3.1.资源访问
宿主程序调起未安装的插件apk,一个最大的问题是资源如何访问,具体来说以R开头的资源都不能被访问了。为了方便对插件进行资源管理,下面给出一种方式:
Activity的工作主要是通过ContextImpl来完成的,Activity中有一个方法叫做mBase的成员变量,它的类型是ContextImpl,注意到Context中有如下两个抽象方法,看起来和资源有关的,实际上Context就是通过他们来获取资源的,需要实现这两个方法:
public abstract AssetManager getAssets() ;
public abstract Resources getResources();
步骤一:加载APK的资源,通过反射调用获取AssetManager的addAssetPath方法,并将APK的资源加载至Resources对象中。
private void loadResources() {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
addAssetPath.invoke(assetManager,mDexPath);
mAssetManager = assetManager;
} catch (Exeception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
步骤二:直接将apk的路径给他,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources,这个就可以访问资源了。
public final int addAssetPath(String path) {
synchronized{
int res = addAssetPathNative(path);
return res;
}
}
步骤三:实现两个抽象方法,可通过R访问插件中的资源问题。
public AssetManager getAssets(){
return mAssetManager==null?super.getAssets():mAssetManager;
}
public Resources getResources(){
return mResources==null?super.getResources():mResources;
}
3.2.Activity的生命周期
管理Activity生命周期管理的方式各种各样,这里介绍两种:反射方式和接口方式。反射方式通过Java反射获取Activity的各种声明周期,譬如:onCreate、onStart、onResume等,然后再代理Activity中调用插件Activity的生命周期,如下所示:
@Override
protected void onResume() {
super.onResume();
Method onResume = mActivityLifecircleMethods.get("onResume");
if(onResume != null){
try {
onResume.invoke(this,new Object[]{});
} catch (Execption e) {
e.printStackTrace();
}
}
}
@Override
protected void onPause() {
super.onPause();
Method onPause = mActivityLifecircleMethods.get("onPause");
if(onPause != null){
try {
onPause.invoke(this,new Object[]{});
} catch (Execption e) {
e.printStackTrace();
}
}
}
反射管理插件Activity的生命周期的缺点:一方面是反射代码写起来比较复杂,另一方面是过多的使用反射有一定的性能开销。接口方法解决它的不足。这种方式将Activity的生命周期提取出来作为一个接口(DLPlugin),然后通过代理Activity去调用插件Activity的生命周期方法,这样完成生命周期的管理,并且没有采用反射,解决了性能问题。
public interface DLPlugin {
public void onStart();
public void onRestart();
public void onActivityResult(int requestCode, int resultCode, Intent data);
public void onResume();
public void onPause();
public void onStop();
public void onDestroy();
public void onCreate(Bundle savedInstanceState);
public void setProxy(Activity proxyActivity,String dexPath);
public void onSaveInstanceState(Bundle outState);
public void onNewIntent(Intent intent);
public void onRestoreInstanceState(Bundle savedInstanceState);
public void onTouchEvent(MotionEvent event);
public void onKeyUp(int keyCode, KeyEvent event);
public void onWindowAttributesChanged(ViewGroup.LayoutParams params);
public void onWindowFocusChanged(boolean hasFocus);
public void onBackPressed();
}
代理Activity只需要按如下方式调用Activity的生命周期方法。
....
@Override
protected void onStart() {
super.onStart();
mRemoteActivity.onStart();
}
....
3.3.插件ClassLoader的管理
为了更好地对多插件进行支持,需要合理的去管理各个插件的DexClassoader,这样同一个插件就可以采用同一个ClassLoader去加载类,从而避免了多个ClassLoader加载同一个类所引发的类型换错误。譬如:使用HashMap存储了不同插件的ClassLoader,确保互不干扰。
public class DLClassLoader extends DexClassLoader {
private static final String TAG = "DLClassLoader";
private static final HashMap<String, DLClassLoader> mPluginClassLoader = new HashMap<>();
public DLClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
}
public static DLClassLoader getClassLoader(String dexPath, Context mContext,ClassLoader parentLoader){
DLClassLoader dlClassLoader = mPluginClassLoader.get(dexPath);
if(dlClassLoader != null){
return dlClassLoader;
}
File dexOutputDir = mContext.getDir("dex",Context.MODE_PRIVATE);
final String dexOutputPath = dexOutputDir.getAbsolutePath();
dlClassLoader = new DLClassLoader(dexPath,dexOutputPath,null,parentLoader);
mPluginClassLoader.put(dexPath,dlClassLoader);
return dlClassLoader;
}
}
插件化技术较为复杂。
(4)反编译初步
使用dex2jar和jd-gui反编译APK,使用apktool对apk进行二次打包。解包、二次打包、签名等等。通过Apktool解包后,可以看到smali文件和资源文件,修改smali得以修改APK的执行逻辑,二次打包后不能直接安装,需要签名才可以。签名是通过signAPK.jar来完成的,最终得到山寨的APK。Dex2jar的源码分析参考:blog.csdn.net/new_abc/art…