Android ClassLoad原理解读及热修复技术使用

一、Android中的ClassLoad

1、Android中的ClassLoad分为以下几种:

    BootClassLoad:主要加载framework层的字节码文件

    PathClassLoad:加载我们已经安装到系统中的apk文件中的class文件

    DexClassLoad:加载指定目录中的class字节码文件。

    BaseDexClassLoad:PathClassLoad和DexClassLoad的父类。

    一个应用至少会用到BootClassLoad和PathClassLoad这两个ClassLoad,为什么这么讲呢?我们可以验证一下:

    创建一个应用:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
      
        ClassLoader classLoader = getClassLoader();
        if (classLoader != null) {
            Log.i("---hotfix---", "classLoader: " + classLoader.toString());
            while (classLoader.getParent() != null) {
                classLoader = classLoader.getParent();
                Log.i("---hotfix---", "classLoader: " + classLoader.toString());
            }
        }

    }
}

输出:

I/---hotfix---: classLoader: dalvik.system.PathClassLoader[DexPathList[[...]]]
I/---hotfix---: classLoader: java.lang.BootClassLoader@153524

其实也很好理解:一个是加载系统的ClassLoad,一个是加载apk的ClassLoad。这两个肯定是少不了的。

2、ClassLoad特点及作用

ClassLoad的特点:

        双亲代理(委托)模式。ClassLoad在加载一个字节码的时候,首先会询问当前的ClassLoad是否已经加载过此类,如果已经加载过,直接返回,不再重复加载;如果没有加载过,会查询他的parent是否已经加载过,如果parent已经加载过,我们直接返回parent加载过的字节码文件。而如果我们整个继承线路上的ClassLoad都没有加载过此类,才由子ClassLoad去加载。

        这么做的好处是什么呢?很明显,如果我们一个类被位于树中的任意一个ClassLoad加载过,那么在整个系统的生命周期中,这个类都不会被重复加载,大大提高了我们加载类的效率。

ClassLoad两大作用:

      1、类加载的共享功能。Framework层的类一旦被我们的顶层ClassLoad加载过,那么它就会缓存在内存里面,以后我们任何地方用到,都不会重新加载了。

     2、类加载的隔离功能。不同继承路线上的ClassLoad加载的类肯定不是同一个类,就避免了用户自己去写一些代码冒充核心类库来访问我们核心类库中的可见的成员变量。

3、源码解读

ClassLoader.loadClass方法:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

可以看出,明显的双亲委托模式。继续往下看findClass方法:

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

是一个空实现,那么其实现逻辑必定在其子类中实现。他最终是在BaseDexClassLoad中实现,源码如下:

    private final DexPathList pathList;
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

而BaseDexClassLoad的findClass方法中其实是调用了DexPathList中的findclass方法:

	public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

而DexPathList中的findclass方法最终调用了DexFile的loadClassBinaryName方法:

    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, suppressed);
    }

    private static Class defineClass(String name, ClassLoader loader, long cookie,
                                     List<Throwable> suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }
    private static native Class defineClassNative(String name, ClassLoader loader, long cookie) throws ClassNotFoundException, NoClassDefFoundError;    

loadClassBinaryName方法最终调用了defineClassNative方法完成查找,主要逻辑在Native方法中实现,抱歉看不了了。

4、讲完了ClassLoad的原理,我们来看一下,Android中要实现动态加载,难点在哪里?

    A、 有许多组件类需要注册才能使用。比如activity、service等是需要在AndroidManifest注册之后才能使用的。

    B、 资源的动态加载很复杂。Android是把这些资源用对应的id注册的,使用的时候,通过这些id从resource中获取对应的资源。所以如果我们是运行时动态的加载进来的新类,那么类里面用到的资源就会抛出找不到异常,因为我们新类的资源并没有在系统中进行注册。

    C、 Android每个版本对类和资源的加载和注册方式都有不同,所以适配上也是会非常的复杂。

    所以,Android程序运行需要一个上下文环境。提供主题、资源、查询组件。那么我们如何给动态加载进来的类和资源提供一个上下文环境呢?这个就是一些第三方的动态加载库需要解决的核心问题。

5、热修复有哪些流行技术?效果怎么样?

    QQ空间超级补丁方案

    微信的Thinker

    阿里的AndFix,dexposed

    美团的Robust、ele的migo、百度的hotfix

这里借用一张图(侵删)

6、AndFix介绍及apkPatch工具使用介绍

官网:https://github.com/alibaba/AndFix

被当做库library来使用,说明操作起来容易。支持2.3-7.0 Android版本。.apatch为后缀名的差异包。



替换方法来实现bug修复。使有bug的方法永远不会执行到。

对差异方法添加注解进行替换,替换的时候是通过native方法完成的。所以andfix的兼容性并没有那么强。因为他是通过native层的。而native层可能通过版本的不同而改变。每产生一个运行时机制,andfix都要去适配它。

修复流程:

下载差异包生成工具:

共有3个文件:

我是在win系统中,使用apkpatch.bat,如果你是linux,使用apkpatch.sh

执行.bat文件,可以查看命令及解释。

-f :from,新apk的文件路径。

-t : to , 旧apk的文件路径

-o : out,输出文件的路径,就是patch包的路径

-k : keystore,签名文件的路径

-p :kpassword ,签名文件的密码

-a : 签名文件的别名

-e : 别名的密码

7、AndFix使用:

第一步:添加依赖

compile 'com.alipay.euler:andfix:0.5.0@aar'

第二步:初始化

为避免第三方依赖对我们代码的侵入,新建一个管理类管理AndFix的api

/**
 * First of all , you should add dependencie "compile 'com.alipay.euler:andfix:0.5.0@aar'" in your app' build.gradle .
 * Pay attention to the version update by visiting "https://github.com/alibaba/AndFix" .
 * <p>
 * Created by ZLM on 2018/6/23.
 * Describe manage apis of andfix
 */

public class AndFixPatchManager {

    private static AndFixPatchManager sInstance = null;
    private static PatchManager sPatchManager;

    public static AndFixPatchManager getInstance() {
        if (sInstance == null) {
            synchronized (AndFixPatchManager.class) {
                if (sInstance == null) {
                    sInstance = new AndFixPatchManager();
                }
            }
        }
        return sInstance;
    }

    //init AndFix
    public void initPatch(Context context) {
        // Initialize PatchManager
        sPatchManager = new PatchManager(context);
        sPatchManager.init(getCurrentAppVersionName(context));
        // Load patch
        sPatchManager.loadPatch();
    }

    //add patchFile
    public void addPatch(String path) {
        try {
            if (sPatchManager != null && !TextUtils.isEmpty(path)) {
                sPatchManager.addPatch(path);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }


    private static String getCurrentAppVersionName(Context context) {
        String versionName = "1.0.0";
        try {
            PackageManager packageManager = context.getPackageManager();
            PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
            versionName = packageInfo.versionName;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return versionName;
    }
}

初始化:

public class MaterialApplication extends Application {
    public static Context context;
    public static Handler mainHandler;
    private static final String APATCH_PATH     = "/example_patch/";
    public static        String patchFileString = "";


    @Override
    public void onCreate() {
        super.onCreate();
        context = this;
        mainHandler = new Handler();
        //init AndFix
        patchFileString = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
        File file = new File(patchFileString);
        if (file == null || !file.exists()) {
            file.mkdir();
        }
        AndFixPatchManager.getInstance().initPatch(this);

    }
}

第三步:在适当的位置加载差异包。当然差异包的名字可以自己定义,路径也看你心情。注意运行时权限的问题,这里是要存储权限的。

AndFixPatchManager.getInstance().addPatch(MaterialApplication.patchFileString + "out.apatch");

8、假定这时候我分别有一个bug包,一个改过bug的新包,这时候就可以使用差异包工具生成我们的差异包了。你可以新建一个out文件夹存放你的差异包。

在你的apkPatch文件目录汇总中进入客户端窗口。

执行:

 .\apkpatch.bat -f .\new.apk -t .\old.apk -o .\out\ -k .\example.jks -p 111111 -a example -e 111111

当然这里你需要把签名文件、密码、apk包名全部换成你自己的。。。

完成之后,会提示你那个地方方法有了替换,如:

add modified Method:V  print()  in Class:Lnet/feelingtech/example_work/ui/fragment/AnalysisFragment;

进入out文件夹,后缀为apatch的文件就是你的差异包,修改成与你程序中设定的名字一样。把这个补丁文件放到服务器的下载地址去,这时候你就可以将这个差异包更新到应用中,当用户更新差异包后,bug已经被我们修复了!

AndFix使用讲到这里,如有疑问,欢迎交流,我们共同学习。

欢迎关注我的公众号OpenShare,刚刚创建的公众号,专注移动开发和大数据技术学习和分享,也希望通过公众号,促进自己学习和进步。技术博文会同步分享至公众号,欢迎订阅!





猜你喜欢

转载自blog.csdn.net/aminy123/article/details/80790943