Android开发艺术探索——第十三章:综合技术

这章主要是说如何收集Crash,如何解决65536问题,动态加载的方案以及反编译

一.使用CrashHandler来获取应用的crash

Android不可避免的会发生crash,也称之为崩溃,无论你的程序写得有多么完美,总是无法完全避免崩溃的存在。有可能java层也有可能底层,所以我们需要收集到相关的日志来解决问题,所以Thread给我们提供了一个setDefaultUncaughtExceptionHandler

    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) {
        Thread.defaultUncaughtHandler = handler;
    }

从字面的意思好像是设置系统的默认异常处理器,其实这个方法可以解决上面所提到的一些崩溃问题,系统在崩溃的时候回回调UncaughtExceptionHandler的uncaughtException方法中就可以获取到异常信息,可以选择把异常信息存储到SD卡里,然后再合适的时机通过网络将crash信息上传到服务器,这个有点类似于Bugly的原理,这样开发人员就能扑捉到了,还能报错的时候来一些温馨的提示

有了上面的分析,我们就可以着手去实现一个crash捕捉了

public class CrashHandler implements Thread.UncaughtExceptionHandler{

    private static final String TAG = "CrashHandler";

    private Context mContext;
    private static CrashHandler mInstance = new CrashHandler();

    private Thread.UncaughtExceptionHandler mDefCrashHandler;

    private CrashHandler(){}

    public static CrashHandler getInstance(){
        return mInstance;
    }

    public void init(Context context){
        mDefCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
        mContext = context.getApplicationContext();
    }

    @Override
    public void uncaughtException(Thread thread, Throwable throwable) {
        Log.e(TAG,throwable.toString());
        //这里保存错误信息
        if(mDefCrashHandler != null){
            mDefCrashHandler.uncaughtException(thread,throwable);
        }else {
            Process.killProcess(Process.myPid());
        }

    }
}

使用的话直接在Applicationinit即可,这样就能扑捉到异常了

二.使用muitidex来解决方法数越界

在Android中单个dex文件所包含的最大方法数不能超过65536,这包含Android的Framework,依赖的jar以及自己本身的所有方法,65536是一个很大的数,一般来说一个简单的应用方法数很难达到65536,但是那些大型的应用,还是有可能的,针对这个问题,谷歌在2014年提出了muitidex
解决方案

在5.0之前使用muitidex需要引用google提供的jar,在sdk的目录下:androidSdk\extras\android\support\multidex\library\libs,5.0后,android默认已经支持multidex,他可以从apk加载多个dex文件,Multidex方案主要针对AS和Gradle编译环境,如果是Eclipse和ANT就复杂一些了

AS使用比较简单,在app/build.gradle下 android/defaultConfig下 multiDexEnabled true,然后引入依赖

implementation 'com.android.support:multidex:1.0.0'

然后在Application中加入:

MultiDex.install(this);

当然,我们还可以根据gradle规则来配置一些我们想要的功能,在afterEvaluate区域采用–main-dex-list选项来指定dex中要包含的类

afterEvaluate {
    println "afterEvaluate"
    tasks.matching {
        it.name.startsWith("dex")
    }.each { dx ->
        def listFile = project.rootDir.absolutePath + '/app/muindexlist.txt'
        println "root dir:" + project.rootDir.absolutePath
        println "dex task found:" + dx.name
        if(dx.additionalParameters == null){
            dx.additionalParameters = []
        }
        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中则指定了一系列的类,所有在maindexlist.txt中的类都会打包到主dex中,并且这个txt可以被修改,但是他的格式需要规范

com/liuguilin/androidsample/MainActivity

//multidex
android/support/multidex/MultiDex.class
android/support/multidex/MultiDexApplication.class
android/support/multidex/MultiDexExtractor.class
android/support/multidex/MultiDexExtractor$1.class
android/support/multidex/MultiDex$V4.class
android/support/multidex/MultiDex$V14.class
android/support/multidex/MultiDex$V19.class
android/support/multidex/ZipUtil.class
android/support/multidex/ZipUtil$CentralDirectory.class

需要注意的是,multidex的jar包中的9个类必须也要打包到主dex中,窦否则会出异常,这是因为Application对象被创建以后会在attachBaseContext方法中通过install来加载其他的dex文件,这个时候如果相关的类不在主dex中,很显然这些都无法加载。

Multidex方法虽然很好的解决了数组越界的问题,但是还是带来了一些问题

  • 1.应用的启动速度回降低,由于应用启动时会加载额外的dex,这就导致应用的启动速度降低,甚至出现ANR现象,尤其是其他dex文件较大的时候,因此要避免生成较大的dex
  • 2.由于Dalvik linearAlloc的bug,这可能导致Multidex的应用无法再Android4.0以前的系统运行,而且可能产生大量的内存消耗情况

在实际的项目总,第一个是客观存在的,但是第二个极少碰到,所以综合来看,Multidex算是一个非常好的解决方案了。

三.Android的动态加载技术

动态加载技术也叫插件化,在技术驱动型的公司扮演一个相当重要的角色,当项目越来越庞大的时候,需要通过插件化来减轻应用的内存和CPU占用,还可以实现热插拔,即使不发布新版本也可以更新,动态加载是一项非常复杂的技术,这里主要是讲三个基础性问题

不同的插件化方案各有各的特色,但是他们都必须解决三个基础性的问题:资源访问,Activity生命周期的管理和ClassLoader的管理,在介绍他们之前,首先我们要明白宿主和插件的概念,宿主指的是普通的APK,而插件一般是指经过处理的dex或者apk,在主流的插件化框架中多采用经过特殊处理的apk作为插件,处理方式往往和编译以及打包环节有关,另外很多插件化框架都需要用到代理Activity的概念,插件Activity的启动大多数是借助一个代理Activity实现的

  • 1.资源访问

宿主是APK的话,想调用未安装的插件Apk,有一个比较大的问题就是资源访问,具体来说就是插件中凡是R开头的资源都不能访问,这是因为宿主程序并没有插件的资源,所以通过R来加载是行不通的,针对这个问题,有人提出将插件中的资源在数组和插件各持一份,但是这样有问题,就是增加了宿主的大小,其次在这种模式下每次发布一个插件都需要将资源复制到宿主中,这以为着没发布一个插件都要更新一下宿主,这违背了插件化的思想,因为插件化的思想就是减小宿主程序APK包的大小,同时降低宿主程序的更新频率并做到自由装载模块,所以这个方法不可取,他限制了插件在线上更新这一重要特性,还有人提供了一种方式,就是将资源解压后去读娶,这也不是很好

我们知道,Activity的工作主要是通过ContextImpl来完成的,Activity中有一个方法叫做mBase的成员变量,他的类型是ContextImpl,注意到Context中有如下两个抽象方法,看起来和资源有关的,实际上Context就是通过他们来获取资源的,

    @Override
    public AssetManager getAssets() {
        return getResources().getAssets();
    }

    @Override
    public Resources getResources() {
        return mResources;
    }

来看下加载apk资源的示例代码:

    private void loadResources() {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
            addAssetPath.invoke(assetManager,mDexPath);
            mAssetManager = assetManager;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        Resources superRes = super.getResources();
        mResources = new Resources(mAssetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
        mTheme = mResources.newTheme();
        mTheme.setTo(super.getTheme());
    }

从loadResources可以看出,加载资源的方法是通过反射,通过调用AssetManager的addAssetPath方法,我们可以将一个apk的资源加载到Resources对象中,由于addAssetPath是因此的方法,所以无法直接调用,只能通过反射,下面是他的声明,通过注释我们可以看出,传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径给他,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources,这个就可以访问资源了,这样问题就解决了

    public final int addAssetPath(String path) {
        int res = addAssetPathNative(path);
        return res;
    }

接着在代理Activity中实现getAssets()和getResources(),如下所示,关于代理Activity的含义清参看DL开源

    public AssetManager getAssets(){
        return mAssetManager==null?super.getAssets():mAssetManager;
    }

    public Resources getResources(){
        return mResources==null?super.getResources():mResources;
    }

通过上述的步骤,就可以通过R文件访问插件的资源了

  • 2.Activity生命周期管理

管理Activity生命周期管理的方式各种各样,这里介绍两种:反射方式和接口方式,反射的方式很好理解,首先通过JAVA的反射区获取Activity的各种生命周期方法,比如onCreate等,然后在代理Activity中去调用插件Activity对应的生命周期方法即可:

    @Override
    protected void onResume() {
        super.onResume();
        Method onResume = mActivityLifecircleMethods.get("onResume");
        if(onResume != null){
            try {
                onResume.invoke(this,new Object[]{});
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        Method onPause = mActivityLifecircleMethods.get("onPause");
        if(onPause != null){
            try {
                onPause.invoke(this,new Object[]{});
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException 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调用即可

  • 3.插件ClassLoader的管理

为了更好的对多插件进行支持,需要合理的去管理各个插件的DexClassoader,这样同一个插件就可以采用同一个ClassLoader去加载类,从而避免了多个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;
    }
}

事实上插件化的技术细节非常的多,这绝非一个章节的内容所能描述清楚的,另外插件化作为一个核心的技术,需要开发者有较深的开发功底还能比较好理解

四.反编译初步

反编译属于逆向工程中的一种,反编译有很多高级的手段和工具,这里介绍dex2jar和jd-gui来反编译一个aopk,另一个方面介绍apktool来对apk进行二次打包。

1.使用dex2jar和jd-gui反编译apk

dex2jar和jd-gui在很多操作系统上都可以使用,我们这里讲window和linux,Dex2jar是一个将dex文件转换为jar包的工具,dex文件源于apk,

我们需要使用命令

//linux
./dex2jar.sh classes.dex
//window
dex2jar.bat classes.dex

jd-gui可以查看jar的源代码这里不多说

2.使用ktool对apk进行二次打包

解包:

./apktool d -f Test.apk NewTest.apk

二次打包:

./apktool b Test.apk NewTest.apk

签名:

java -jar signapk.jar testKey.x509.pem testKey.pk8  Test.apk NewTest.apk

猜你喜欢

转载自blog.csdn.net/qq_26787115/article/details/80999299