《Android开发艺术探索》之综合技术(十五)

206 阅读7分钟

                                                                               第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…