Android优化全面攻略

96
koller
0.3 2019.06.18 10:12* 字数 2902

一 :安装包性能压缩

一个字:删!!删不了就尽量小。

1.图片压缩

图片:apk里面的资源图片 压缩图片
svg图片:一些图片的描述,牺牲CPU的计算能力的,节省空间。
使用的原则:简单的图标。
webp:谷歌现在非常提倡的使用。保存图片比较小。
VP8派生而来的。webp的无损压缩比PNG文件小45%左右,即使PNG进过其他的压缩工具压缩后,
任然可以减小到PNG的28%。

Facebook在用、腾讯、淘宝。
缺点:加载相比于PNG要慢很多。 但是配置比较高。
工具:http://isparta.github.io/

2.插件化 资源动态加载

比如:emoji表情、换肤
动态下载的资源。
一些模块的插件化动态添加。

3.Lint工具 建议优化的点

1)检测没有用的布局 删除
2)未使用到的资源 比如 图片 ---删除
3)建议String.xml有一些没有用到的字符。

4.极限压缩

7zZip工具的使用。

5.Proguard 混淆。

让apk变小。为什么?
1)可以删除注释和不用的代码。
2)将java文件名改成短名a.java,b.java
3)方法名等 CommonUtil.getDisplayMetrix();--》a.a()
4) shrinkResource 去除无用资源

在常规的安装包的优化之外继续压缩---资源文件再压

系统编译完成apk文件以后:
映射关系:res/drawable/ic_launcher.png ----- > 0x7f020000

再做“混淆”:要实现将res/drawable/ic_launcher.png图片改成a.png
将drawable,String,layout的名字继续缩减
比如:R.string.description--->R.string.a
res/drawable/ic_launcher.png图片改成a.png

还可以更加夸张
res/drawable--->r/d
res/value-->r/v
res/drawable/ic_launcher.png图片改成r/d/a.png

读取resources.arsc二进制文件,然后修改某一段一段的字节。
有一段叫做:res/drawable/ic_launcher.png 在自己数组当中的第800位-810位
将这一段第800位-810位替换成改成r/d/a.png 的字节码。

args参数:
demo.apk -config config.xml -7zip 7za.exe -out path/dirName -mapping your-project/path/mapping.txt

腾讯开源工具:AndResGuard 即利用此原理。

https://github.com/shwenzhang/AndResGuard

二: 图片 bitmap 压缩

1.质量压缩 像素大小并不变化

原理:通过算法扣掉了图片中一些相近的像素,达到降低质量的没得
只能降低File的大小,不能降低内存中的bitmap,bitmap是按照width*high来计算的的

只能将图片压缩后保存到本地,或者上传服务器。
Bitmap.compress(CompressFormat format, int quality, OutputStream stream)

2.尺寸压缩

减少单位尺寸上的像素值,真正意义上降低像素。
缓存缩略图,头像等。
int rotatio = 4;// 压缩倍数
Rect rect = new Rect(0,0,bitmap.getWidth() / rotatio,bitmap.getHeight()/rotatio);
Canvas canvas = new Canvas();
canvas.save();
canvas.drawBitmap(bitmap,null,rect,null);
canvas.restore();

3.采样率压缩

insampleSize = 2

3.1//邻近压缩 简单粗暴,容易失真 直接将邻近点的像素当作压缩后的像素。

Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.png",options);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inSampleSize = 8;

3.2//双线性采样率压缩 将附近像素计算权重后确定压缩后的像素

Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.png");
Bitmap compress = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2, bitmap.getHeight()/2, true);

4终极压缩

将SKIA引擎增加哈夫曼算法
利用libjpeg.so实现 --》微信apk也有了libwechatjpeg.so库
具体JNI实现

/*
 * Copyright 2014 http://Bither.net
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "bitherlibjni.h"
#include <string.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <stdio.h>
#include <setjmp.h>
#include <math.h>
#include <stdint.h>
#include <time.h>

//统一编译方式
extern "C" {
#include "jpeg/jpeglib.h"
#include "jpeg/cdjpeg.h"        /* Common decls for cjpeg/djpeg applications */
#include "jpeg/jversion.h"      /* for version message */
#include "jpeg/android/config.h"
}


#define LOG_TAG "jni"
#define LOGW(...)  __android_log_write(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

#define true 1
#define false 0

typedef uint8_t BYTE;

char *error;
struct my_error_mgr {
  struct jpeg_error_mgr pub;
  jmp_buf setjmp_buffer;
};

typedef struct my_error_mgr * my_error_ptr;

METHODDEF(void)
my_error_exit (j_common_ptr cinfo)
{
  my_error_ptr myerr = (my_error_ptr) cinfo->err;
  (*cinfo->err->output_message) (cinfo);
  error=(char*)myerr->pub.jpeg_message_table[myerr->pub.msg_code];
  LOGE("jpeg_message_table[%d]:%s", myerr->pub.msg_code,myerr->pub.jpeg_message_table[myerr->pub.msg_code]);
 // LOGE("addon_message_table:%s", myerr->pub.addon_message_table);
//  LOGE("SIZEOF:%d",myerr->pub.msg_parm.i[0]);
//  LOGE("sizeof:%d",myerr->pub.msg_parm.i[1]);
  longjmp(myerr->setjmp_buffer, 1);
}

int generateJPEG(BYTE* data, int w, int h, int quality,
        const char* outfilename, jboolean optimize) {

    //jpeg的结构体,保存的比如宽、高、位深、图片格式等信息,相当于java的类
    struct jpeg_compress_struct jcs;

    //当读完整个文件的时候就会回调my_error_exit这个退出方法。setjmp是一个系统级函数,是一个回调。
    struct my_error_mgr jem;
    jcs.err = jpeg_std_error(&jem.pub);
    jem.pub.error_exit = my_error_exit;
    if (setjmp(jem.setjmp_buffer)) {
        return 0;
    }

    //初始化jsc结构体
    jpeg_create_compress(&jcs);
    //打开输出文件 wb:可写byte
    FILE* f = fopen(outfilename, "wb");
    if (f == NULL) {
        return 0;
    }
    //设置结构体的文件路径
    jpeg_stdio_dest(&jcs, f);
    jcs.image_width = w;//设置宽高
    jcs.image_height = h;
//  if (optimize) {
//      LOGI("optimize==ture");
//  } else {
//      LOGI("optimize==false");
//  }

    //看源码注释,设置哈夫曼编码:/* TRUE=arithmetic coding, FALSE=Huffman */
    jcs.arith_code = false;
    int nComponent = 3;
    /* 颜色的组成 rgb,三个 # of color components in input image */
    jcs.input_components = nComponent;
    //设置结构体的颜色空间为rgb
    jcs.in_color_space = JCS_RGB;
//  if (nComponent == 1)
//      jcs.in_color_space = JCS_GRAYSCALE;
//  else
//      jcs.in_color_space = JCS_RGB;

    //全部设置默认参数/* Default parameter setup for compression */
    jpeg_set_defaults(&jcs);
    //是否采用哈弗曼表数据计算 品质相差5-10倍
    jcs.optimize_coding = optimize;
    //设置质量
    jpeg_set_quality(&jcs, quality, true);
    //开始压缩,(是否写入全部像素)
    jpeg_start_compress(&jcs, TRUE);

    JSAMPROW row_pointer[1];
    int row_stride;
    //一行的rgb数量
    row_stride = jcs.image_width * nComponent;
    //一行一行遍历
    while (jcs.next_scanline < jcs.image_height) {
        //得到一行的首地址
        row_pointer[0] = &data[jcs.next_scanline * row_stride];

        //此方法会将jcs.next_scanline加1
        jpeg_write_scanlines(&jcs, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数
    }
    jpeg_finish_compress(&jcs);//结束
    jpeg_destroy_compress(&jcs);//销毁 回收内存
    fclose(f);//关闭文件

    return 1;
}

/**
 * byte数组转C的字符串
 */
char* jstrinTostring(JNIEnv* env, jbyteArray barr) {
    char* rtn = NULL;
    jsize alen = env->GetArrayLength( barr);
    jbyte* ba = env->GetByteArrayElements( barr, 0);
    if (alen > 0) {
        rtn = (char*) malloc(alen + 1);
        memcpy(rtn, ba, alen);
        rtn[alen] = 0;
    }
    env->ReleaseByteArrayElements( barr, ba, 0);
    return rtn;
}

jstring Java_net_bither_util_NativeUtil_compressBitmap(JNIEnv* env,
        jclass thiz, jobject bitmapcolor, int w, int h, int quality,
        jbyteArray fileNameStr, jboolean optimize) {
    BYTE *pixelscolor;
    //1.将bitmap里面的所有像素信息读取出来,并转换成RGB数据,保存到二维byte数组里面
    //处理bitmap图形信息方法1 锁定画布
    AndroidBitmap_lockPixels(env,bitmapcolor,(void**)&pixelscolor);

    //2.解析每一个像素点里面的rgb值(去掉alpha值),保存到一维数组data里面
    BYTE *data;
    BYTE r,g,b;
    data = (BYTE*)malloc(w*h*3);//每一个像素都有三个信息RGB
    BYTE *tmpdata;
    tmpdata = data;//临时保存data的首地址
    int i=0,j=0;
    int color;
    for (i = 0; i < h; ++i) {
        for (j = 0; j < w; ++j) {
            //解决掉alpha
            //获取二维数组的每一个像素信息(四个部分a/r/g/b)的首地址
            color = *((int *)pixelscolor);//通过地址取值
            //0~255:
//          a = ((color & 0xFF000000) >> 24);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));
            //改值!!!----保存到data数据里面
            *data = b;
            *(data+1) = g;
            *(data+2) = r;
            data = data + 3;
            //一个像素包括argb四个值,每+4就是取下一个像素点
            pixelscolor += 4;
        }
    }
    //处理bitmap图形信息方法2 解锁
    AndroidBitmap_unlockPixels(env,bitmapcolor);
    char* fileName = jstrinTostring(env,fileNameStr);
    //调用libjpeg核心方法实现压缩
    int resultCode = generateJPEG(tmpdata,w,h,quality,fileName,optimize);
    if(resultCode ==0){
        jstring result = env->NewStringUTF("-1");
        return result;
    }
    return env->NewStringUTF("1");
}

FFmpeg中有双立方采样压缩跟三线性采样压缩,但是未引用FFmpeg的android项目无法使用,故采用上面这种方式。

三:Splash启动优化

提升应用的启动速度 和 splash页面的设计

1.启动分为两种方式:

1)冷启动:当直接从桌面上直接启动,同时后台没有该进程的缓存,这个时候系统就需要
重新创建一个新的进程并且分配各种资源。
2)热启动:该app后台有该进程的缓存,这时候启动的进程就属于热启动。

热启动不需要重新分配进程,也不会Application了,直接走的就是app的入口Activity,这样就速度快很多

2.如何测量一个应用的启动时间

使用命令行来启动app,同时进行时间测量。单位:毫秒
adb shell am start -W [PackageName]/[PackageName.MainActivity]
adb shell am start -W project.study.koller.mystudyproject.app/project.study.koller.mystudyproject.app.MainActivity


ThisTime: 165 指当前指定的MainActivity的启动时间
TotalTime: 165 整个应用的启动时间,Application+Activity的使用的时间。
WaitTime: 175 包括系统的影响时间---比较上面大。

3.应用启动的流程

Application从构造方法开始--->attachBaseContext()--->onCreate()
Activity构造方法--->onCreate()--->设置显示界面布局,设置主题、背景等等属性
--->onStart()--->onResume()--->显示里面的view(测量、布局、绘制,显示到界面上)

时间花在哪里了?

4.减少应用的启动时间的耗时

1)、不要在Application的构造方法、attachBaseContext()、onCreate()里面进行初始化耗时操作。
2)、MainActivity,由于用户只关心最后的显示的这一帧,对我们的布局的层次要求要减少,自定义控件的话测量、布局、绘制的时间。
    不要在onCreate、onStart、onResume当中做耗时操作。
3)、对于SharedPreference的初始化。
    因为他初始化的时候是需要将数据全部读取出来放到内存当中。
    优化1:可以尽可能减少sp文件数量(IO需要时间);2.像这样的初始化最好放到线程里面;3.大的数据缓存到数据库里面。

app启动的耗时主要是在:Application初始化 + MainActivity的界面加载绘制时间。

由于MainActivity的业务和布局复杂度非常高,甚至该界面必须要有一些初始化的数据才能显示。
那么这个时候MainActivity就可能半天都出不来,这就给用户感觉app太卡了。

我们要做的就是给用户赶紧利落的体验。点击app就立马弹出我们的界面。
于是乎想到使用SplashActivity--非常简单的一个欢迎页面上面都不干就只显示一个图片。

但是SplashActivity启动之后,还是需要跳到MainActivity。MainActivity还是需要从头开始加载布局和数据。
想到SplashActivity里面可以去做一些MainActivity的数据的预加载。然后需要通过意图传到MainActivity。

可不可以再做一些更好的优化呢?
耗时的问题:Application+Activity的启动及资源加载时间;预加载的数据花的时间。

如果我们能让这两个时间重叠在一个时间段内并发地做这两个事情就省时间了。

解决:

将SplashActivity和MainActivity合为一个。

一进来还是现实的MainActivity,SplashActivity可以变成一个SplashFragment,然后放一个FrameLayout作为根布局直接现实SplashFragment界面。
SplashFragment里面非常之简单,就是现实一个图片,启动非常快。
当SplashFragment显示完毕后再将它remove。同时在splash的2S的友好时间内进行网络数据缓存。
这个时候我们才看到MainActivity,就不必再去等待网络数据返回了。

问题:SplashView和ContentView加载放到一起来做了 ,这可能会影响应用的启动时间。
解决:可以使用ViewStub延迟加载MainActivity当中的View来达到减轻这个影响。

viewStub的设计就是为了防止MainActivity的启动加载资源太耗时了。延迟进行加载,不影响启动,用户友好。
但是viewStub加载也需要时间。等到主界面出来以后。
viewStub.inflate(xxxx);

5.如何设计延迟加载DelayLoad

第一时间想到的就是在onCreate里面调用handler.postDelayed()方法。
问题:这个延迟时间如何控制?
不同的机器启动速度不一样。这个时间如何控制?
假设,需要在splash做一个动画--2S

需要达到的效果:应用已经启动并加载完成,界面已经显示出来了,然后我们再去做其他的事情。

如果我们这样:

        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mProgressBar.setVisibility(View.GONE);
                iv.setVisibility(View.VISIBLE);
            }
        }, 2500);

是没法做到等应用已经启动并加载完成,界面已经显示出来了,然后我们再去做其他的事情。2500ms并不能适配所有手机(CPU,内存大小,都不一样)

问题:什么时候应用已经启动并加载完成,界面已经显示出来了。
onResume执行完了之后才显示完毕。
利用getWindow().getDecorView().post(Runable)即可
getWindow().getDecorView().post()只是进入了第一次的perfromTraversal的后面 接下来在里面再次post 则是进入了第二次perfromTraversal后面,即在第一帧绘制完成之后才进行UI更新。

public class MainActivity extends FragmentActivity {

    private Handler mHandler = new Handler();
    private SplashFragment splashFragment;
    private ViewStub viewStub;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        splashFragment = new SplashFragment();
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.replace(R.id.frame, splashFragment);
        transaction.commit();
        
//      mHandler.postDelayed(new Runnable() {
//          @Override
//          public void run() {
//              mProgressBar.setVisibility(View.GONE);
//              iv.setVisibility(View.VISIBLE);
//          }
//      }, 2500);
        
        viewStub = (ViewStub)findViewById(R.id.content_viewstub);
        //1.判断当窗体加载完毕的时候,立马再加载真正的布局进来
        getWindow().getDecorView().post(new Runnable() {
            
            @Override
            public void run() {
                // 开启延迟加载
                mHandler.post(new Runnable() {
                    
                    @Override
                    public void run() {
                        //将viewstub加载进来
                        viewStub.inflate();
                    }
                } );
            }
        });
        
        
        //2.判断当窗体加载完毕的时候执行,延迟一段时间做动画。
        getWindow().getDecorView().post(new Runnable() {
            
            @Override
            public void run() {
                // 开启延迟加载,也可以不用延迟可以立马执行(我这里延迟是为了实现fragment里面的动画效果的耗时)
                //mHandler.postDelayed(new DelayRunnable(MainActivity.this, splashFragment) ,2000);
                
                mHandler.post(new DelayRunnable());
                
            }
        });
        //3.同时进行异步加载数据
        
        
    }
    
    @Override
    protected void onResume() {
        // TODO Auto-generated method stub
        super.onResume();
    }
    
    static class DelayRunnable implements Runnable{
        private WeakReference<Context> contextRef;
        private WeakReference<SplashFragment> fragmentRef;
        
        public DelayRunnable(Context context, SplashFragment f) {
            contextRef = new WeakReference<Context>(context);
            fragmentRef = new WeakReference<SplashFragment>(f);
        }

        @Override
        public void run() {
            // 移除fragment
            if(contextRef!=null){
                SplashFragment splashFragment = fragmentRef.get();
                if(splashFragment==null){
                    return;
                }
                FragmentActivity activity = (FragmentActivity) contextRef.get();
                FragmentTransaction transaction = activity.getSupportFragmentManager().beginTransaction();
                transaction.remove(splashFragment);
                transaction.commit();
                
            }
        }
        
    }

}

四: 进程保活

Service

service:是一个后台服务,专门用来处理常驻后台的工作的组件。

即时通讯:service来做常驻后台的心跳传输。
1.良民:核心服务尽可能地轻!!!
很多人喜欢把所有的后台操作都集中在一个service里面。
为核心服务专门做一个进程,跟其他的所有后台操作隔离。
树大招风,核心服务千万要轻。

一、优先级

进程的重要性优先级:(越往后的就越容易被系统杀死)
1.前台进程;Foreground process
1)用户正在交互的Activity(onResume())
2)当某个Service绑定正在交互的Activity。
3)被主动调用为前台Service(startForeground())
4)组件正在执行生命周期的回调(onCreate()/onStart()/onDestroy())
5)BroadcastReceiver 正在执行onReceive();

2.可见进程;Visible process
1)我们的Activity处在onPause()(没有进入onStop())
2)绑定到前台Activity的Service。

3.服务进程;Service process
简单的startService()启动。
4.后台进程;Background process
对用户没有直接影响的进程----Activity出于onStop()的时候。
android:process=":xxx"
5.空进程; Empty process
不含有任何的活动的组件。(android设计的,为了第二次启动更快,采取的一个权衡)

二、如何提升进程的优先级(尽量做到不轻易被系统杀死)

1.QQ采取在锁屏的时候启动一个1个像素的Activity,当用户解锁以后将这个Activity结束掉(顺便同时把自己的核心服务再开启一次)。被用户发现了就不好了。

故事:小米撕逼。
背景:当手机锁屏的时候什么都干死了,为了省电。
锁屏界面在上面盖住了。
监听锁屏广播,锁了---启动这个Activity。
监听锁屏的, 开启---结束掉这个Activity。
要监听锁屏的广播---动态注册。
ScreenListener.begin(new xxxListener
onScreenOff()
);

被系统无法杀死的进程。

2.app运营商和手机厂商可能有合作关系---白名单。

3.双进程守护---可以防止单个进程杀死,同时可以防止第三方的360清理掉。

一个进程被杀死,另外一个进程又被他启动。相互监听启动。

A<--->B
杀进程是一个一个杀的。本质是和杀进程时间赛跑。

4.JobScheduler

把任务加到系统调度队列中,当到达任务窗口期的时候就会执行,我们可以在这个任务里面启动我们的进程。
这样可以做到将近杀不死的进程。

5.监听QQ,微信,系统应用,友盟,小米推送等等的广播,然后把自己启动了。

6.利用账号同步机制唤醒我们的进程

AccountManager

7.NDK来解决,Native进程来实现双进程守护。

数据传输效率优化-flatBuffer

一、数据的序列化和反序列化

服务器对象Object----流--->客户端Object对象

序列化:
Serializable/Parcelable

时间:1ms * 10 * 50 * 20 = 10000ms
性能:内存的浪费和CPU计算时间的占用。

json/xml
json序列化的工具GSON/fastjson

FlatBuffer:基于二进制的文件。
json:基于字符串的

五:插件化-VIrtualAPK 加载逻辑

VirtualAPK加载逻辑
1 宿主APPlication,新建PluginManager的Instance
2 PluginManager构造函数中Hook了Instrumention,Handler,AMS,新建ComponentHanlder
3 MainActiviy 加摘APK,PluginManager.LoadPlugin(apk)
4 创建LoadedPlugin 加载APK,缓存四大组件及Instrumention
5 尝试启动插件apk的application. 利用Instrumention.newApplication(DexClassloader,applicatoinClassName,activity的context);
6 给application注册activitylifecycle的回调监听
7 点击启动按钮,拦截execStartActivity方法,将目标Acitivity(即插件Acitivity)替换为占位Activity,即替换Intent.
8 拦截newActivity方法,先尝试用当前的classLoader加载参数class,如果抛出ClassNotFoundException即为插件Activity(因为占位Activity并没有实体,仅在Manifest中注册),还原目标Intent,调用newActivity启动,并利用反射获取资源文件夹,将其赋值给插件Activity。

9 插件化出现android.view.inflateException是因为资源没有被加载进来,所以报错,资源找不到所以报错。用纯代码方式布局,无问题.

10 在小米9上(android9.0 MiUi10.2) VitrualAPK启动demo也有问题。

六:热修复

1 将bug的类修改完毕
2 利用dx工具(androidSdk自带)将其打包为dex文件
3 将dex文件下载到手机
4 利用DexClassLoader跟PathClassLoader的共同父类BaseDexLoader里面的DexPathList的dexElements数组,将其插入到数组最前面即可。

class BaseDexClassLoader{
    DexPathList pathList;
}
class DexPathList{
    Element[] dexElements;
}

1.找到MyTestClass.class
project_name\app\build\intermediates\bin\MyTestClass.class
2.配置dx.bat的环境变量
Android\sdk\build-tools\23.0.3\dx.bat
3.命令
dx --dex --output=D:\Users\adminstor\Desktop\dex\classes2.dex D:\Users\adminstor\Desktop\dex
命令解释:
--output=D:\Users\adminstor\Desktop\dex\classes2.dex 指定输出路径
D:\Users\adminstor\Desktop\dex 最后指定去打包哪个目录下面的class字节文件(注意要包括全路径的文件夹,也可以有多个class)

然后利用反射将其加载进app,等到下次启动即可。

public class FixDexUtils {
    private static HashSet<File> loadedDex = new HashSet<File>();
    
    static{
        loadedDex.clear();
    }

    public static void loadFixedDex(Context context){
        if(context == null){
            return ;
        }
        //遍历所有的修复的dex
        File fileDir = context.getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
        File[] listFiles = fileDir.listFiles();
        for(File file:listFiles){
            if(file.getName().startsWith("classes")&&file.getName().endsWith(".dex")){
                loadedDex.add(file);//存入集合
            }
        }
        //dex合并之前的dex
        doDexInject(context,fileDir,loadedDex);
    }

    private static void setField(Object obj,Class<?> cl, String field, Object value) throws Exception {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj,value);
    }

    private static void doDexInject(final Context appContext, File filesDir,HashSet<File> loadedDex) {
        String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex";
        File fopt = new File(optimizeDir);
        if(!fopt.exists()){
            fopt.mkdirs();
        }
        //1.加载应用程序的dex
        try {
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();

            for (File dex : loadedDex) {
                //2.加载指定的修复的dex文件。
                DexClassLoader classLoader = new DexClassLoader(
                        dex.getAbsolutePath(),//String dexPath,
                        fopt.getAbsolutePath(),//String optimizedDirectory,
                        null,//String libraryPath,
                        pathLoader//ClassLoader parent
                );
                //3.合并
                Object dexObj = getPathList(classLoader);
                Object pathObj = getPathList(pathLoader);
                Object mDexElementsList = getDexElements(dexObj);
                Object pathDexElementsList = getDexElements(pathObj);
                //合并完成
                Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
                //重写给PathList里面的lement[] dexElements;赋值
                Object pathList = getPathList(pathLoader);
                setField(pathList,pathList.getClass(),"dexElements",dexElements);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Object getField(Object obj, Class<?> cl, String field)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }
    private static Object getPathList(Object baseDexClassLoader) throws Exception {
            return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
    }

    private static Object getDexElements(Object obj) throws Exception {
            return getField(obj,obj.getClass(),"dexElements");
    }

    /**
     * 两个数组合并
     * @param arrayLhs
     * @param arrayRhs
     * @return
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }
}
日记本