精华内容
参与话题
问答
  • 如何实现热更新

    千次阅读 2018-07-12 18:01:45
    热更新的优点热更新是一个绝对很酷的功能.简单来说,它的好处有两点:一个是提高开发效率,一个是在线上修复问题.可能有些同学不太理解,毕竟大家的技术背景不太一样,所以这里还是展开来讨论一下.先说开发效率.我以前...

    热更新的优点

    热更新是一个绝对很酷的功能.简单来说,它的好处有两点:一个是提高开发效率,一个是在线上修复问题.可能有些同学不太理解,毕竟大家的技术背景不太一样,所以这里还是展开来讨论一下.

    先说开发效率.我以前曾经做过一段游戏服务器的开发,与web服务器不太一样的是,游戏服务器通常需要在启动的时候加载很多数据进来.如果你使用的是编译型语言做游戏服务器的开发,那么假如进行了一些修改,除去编码-编译的过程不说,每次重启服务器验证时加载数据的时间都够你上个厕所的.所以,基本上现在做游戏服务器的开发,如果完全使用编译型语言,那么会很苦逼,一般都采用编译型语言结合脚本语言的方式.

    有了脚本语言的热更新功能,还是刚才的工作场景,不仅没有了编译的过程,而且还不需要重启服务器了.在使用了Lua来编写游戏服务器的逻辑之后,我经常一整天都不需要重启服务器,就可以完成整个工作,开发效率提高了很多.

    也正是因为热更新能使得不需要重启服务器就能更新代码,所以也可以用在线上修复问题.

    热更新的缺点

    前面说的只是热更新的优点,也需要提一提热更新的缺点.

    脚本语言虽然不需要编译,但是这也存在一个缺陷,就是代码要到运行时才能跑到,也只有到那个时候才能知道代码有没有问题.换言之,需要非常多的测试覆盖来保证脚本的正确性.

    另外,前面提到热更新能在线上修复问题,但是不能因为这样就过度依赖这个特性,更不能把修复变成了新增功能,热更新修复线上问题是实在万不得已才为之的.

    使用qnode如何实现热更新

    要支持项目的Lua脚本热更新,在qnode中需要做一些特殊的事情.以examples/qcached中的memcached服务器为例来说明.

    该项目中在配置文件中指定加载的主文件是main.lua,来看看它的内容:

    require("util")
    
    require_ex("server")
    require_ex("child")
    

    该项目除了main.lua之外,还有另外两个Lua文件:server.lua和child.lua,所以这里main.lua在启动时调用util提供的函数require_ex来加载这两个文件.

    所有需要支持热更新的文件,在加载的时候都需要使用require_ex函数来加载.

    当修改了代码需要热更新验证时,可以向qnode服务器发送USR1信号来让服务器重新加载,这在根目录中reload.sh脚本中已经封装了该操作.

    简而言之,需要支持热更新的项目文件组织应该是这样的:由配置文件指定的服务器启动脚本文件中,使用require_ex函数来加载所有该项目的脚本文件,在需要热更新的时候发送USR1信号来通知服务器,它将按照顺序重新加载所有的脚本文件.

    展开全文
  • 热更新介绍

    千次阅读 2018-10-02 09:10:32
    什么是热更新:游戏上线后玩家下载的第一个版本(70M~200M),在运营过程中,如果需要更换UI显示,或者修改游戏的逻辑,这个时候,如果不使用热更新,就需要重新打包,然后让玩家重新下载(浪费流量和时间,体验不好...
    • 什么是热更新:游戏上线后玩家下载的第一个版本(70M~200M),在运营过程中,如果需要更换UI显示,或者修改游戏的逻辑,这个时候,如果不使用热更新,就需要重新打包,然后让玩家重新下载(浪费流量和时间,体验不好)。 热跟新可以在不重新下载客户端的情况下更新游戏的内容。 热更新一般用在手机的网游上。
    • 为什么C#脚本不能直接进行更新:C#是一门编程语言,它运行之前需要在编译环境下进行编译,而这个编译过程在移动平台无法完成,所以当我们游戏逻辑更改、C#代码发生改变的时候,我们就需要重新在开发环境下编译(形成dll文件),然后重新打包让玩家下载更新最新的版本。 体验差、浪费流量。
    • 热更新有哪些实现方式:
      1. 使用Lua脚本编写游戏的UI或者其他的逻辑:Lua是一个精悍小巧的脚本语言,可以跨平台进行解析,而不需要编译的过程。
      2. 使用C#light
      3. 使用C#反射技术
    • 关于AssetBundle:Unity提供了一个资源更新技术,就是通过AssetBundle,我们可以通过AssetBundle更新游戏UI,也可以把脚本或者其他代码当成资源打包成AssetBundle然后更新到客户端。在所有的热更新技术中都需要AssetBundle
    • 如何利用Lua进行热更新:在移动端可以编写Lua的解析器,通过这个解析器,可以运行最新的Lua脚本,然后我们把控制游戏逻辑的代码都写成Lua脚本。
    • Lua的解析技术有哪些
      1. uLua 骏擎【CP】 ulua.org
      2. Nlua unity支持Riley G nlua.org
      3. UniLua 阿楠同学
      4. sLua
    展开全文
  • 热更新

    千次阅读 2017-07-25 19:22:28
    虽然有插件开发,但热更新少不了。 当我们需要更新插件的时候使用的是插件开发,当我们需要更新宿主程序就需要使用热更新 热更新使用框架bsdiff和bzip2 我们需要在服务端使用新版apk和旧版apk生成差分包,由于...

    虽然有插件开发,但热更新少不了。


    当我们需要更新插件的时候使用的是插件开发,当我们需要更新宿主程序就需要使用热更新

    热更新使用框架bsdiff和bzip2

    我们需要在服务端使用新版apk和旧版apk生成差分包,由于服务端在windows下,所以生成动态文件(dll)

    通过寻找bsdiff的入口函数在bsdiff.cpp中在,因为截止到现在我主要了解了一点c,所以把bsdiff.cpp改成了bsdiff.c

    int main(int argc,char *argv[])
    {
    	int fd;
    
    	u_char *old,*_new;
    	off_t oldsize,newsize;
    	off_t *I,*V;
    	off_t scan,pos,len;
    	off_t lastscan,lastpos,lastoffset;
    	off_t oldscore,scsc;
    	off_t s,Sf,lenf,Sb,lenb;
    	off_t overlap,Ss,lens;
    	off_t i;
    	off_t dblen,eblen;
    	u_char *db,*eb;
    	u_char buf[8];
    	u_char header[32];
    	FILE * pf;
    
    	BZFILE * pfbz2;
    
    	int bz2err;
    
    
    	if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);
    
    	/* Allocate oldsize+1 bytes instead of oldsize bytes to ensure
    		that we never try to malloc(0) and get a NULL pointer */
    	//org:
    	//if(((fd=open(argv[1],O_RDONLY,0))<0) ||
    
    	//	((oldsize=lseek(fd,0,SEEK_END))==-1) ||
    
    	//	((old=malloc(oldsize+1))==NULL) ||
    
    	//	(lseek(fd,0,SEEK_SET)!=0) ||
    
    	//	(read(fd,old,oldsize)!=oldsize) ||
    
    	//	(close(fd)==-1)) err(1,"%s",argv[1]);
    	//new:
    	//Read in chunks, don't rely on read always returns full data!
    
    	if(((fd=open(argv[1],O_RDONLY|O_BINARY|O_NOINHERIT,0))<0) ||
    		((oldsize=lseek(fd,0,SEEK_END))==-1) ||
    		((old=(u_char*)malloc(oldsize+1))==NULL) ||
    		(lseek(fd,0,SEEK_SET)!=0))
    
    				err(1,"%s",argv[1]);
    
    	int r=oldsize;
    
    	while (r>0 && (i=read(fd,old+oldsize-r,r))>0) r-=i;
    
    	if (r>0 || close(fd)==-1) err(1,"%s",argv[1]);
    
    
    
    
    	if(((I=(off_t*)malloc((oldsize+1)*sizeof(off_t)))==NULL) ||
    		((V=(off_t*)malloc((oldsize+1)*sizeof(off_t)))==NULL)) err(1,NULL);
    
    	qsufsort(I,V,old,oldsize);
    
    	free(V);
    这里贴上前几行代码,只要调用这个函数就能得到差分包(.patch),
    而从if(argc!=4) errx(1,"usage: %s oldfile newfile patchfile\n",argv[0]);这行代码可以知道,argc这个参数必须等于4才可以正常执行,argv数组的第一个参数没有要求,从后面代码得到第二个参数为旧版apk的路径,第三个参数为新版apk的路径,第四个参数为生成的差分包的位置。

    当我们差分的时候只要调用这个main函数并传递相应参数就可以得到差分包了,所以我们修改入口函数的名字,目的是当我们调用的时候采取执行。我们只需要在bsdiff这个文件下实现我们声明的native方法并调用入口函数。然后生成动态库文件放入服务端项目下用来辅助生成差分包

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_dongnaoedu_bsdiff_BsDiff */
    
    #ifndef _Included_com_dongnaoedu_bsdiff_BsDiff
    #define _Included_com_dongnaoedu_bsdiff_BsDiff
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     com_dongnaoedu_bsdiff_BsDiff
     * Method:    diff
     * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
     */
    JNIEXPORT void JNICALL Java_com_dongnaoedu_bsdiff_BsDiff_diff
      (JNIEnv *, jclass, jstring, jstring, jstring);
    
    #ifdef __cplusplus
    }
    #endif
    #endif

    public class BsDiff {
    
    	/**
    	 * 差分
    	 * 
    	 * @param oldfile
    	 * @param newfile
    	 * @param patchfile
    	 */
    	public native static void diff(String oldfile, String newfile, String patchfile);
    
    	static{
    		System.loadLibrary("bsdiff");
    	}
    }

    这里只贴出了部分代码。这样我们就可以得到差分包,在服务器就可以获取这个差分包,然后我们需要从客户端下载这个差分包并与旧版apk进行合并生成新版apk,这样就可以省去部分流量。

    合并的入口函数在bspatch文件中,下面为bspatch文件中native方法的实现,bspatch_main就是改了名字之后的入口函数

    //合并
    JNIEXPORT void JNICALL Java_com_dongnaoedu_appupdate_utils_BsPatch_patch
      (JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr){
    	int argc = 4;
    	char* oldfile = (char*)(*env)->GetStringUTFChars(env,oldfile_jstr, NULL);
    	char* newfile = (char*)(*env)->GetStringUTFChars(env,newfile_jstr, NULL);
    	char* patchfile = (char*)(*env)->GetStringUTFChars(env,patchfile_jstr, NULL);
    
    	//参数(第一个参数无效)
    	char *argv[4];
    	argv[0] = "bspatch";
    	argv[1] = oldfile;
    	argv[2] = newfile;
    	argv[3] = patchfile;
    
    	bspatch_main(argc,argv);
    
    	(*env)->ReleaseStringUTFChars(env,oldfile_jstr, oldfile);
    	(*env)->ReleaseStringUTFChars(env,newfile_jstr, newfile);
    	(*env)->ReleaseStringUTFChars(env,patchfile_jstr, patchfile);
    
    }
    和差分类似,数组的第三个参数为生成的新版本apk路径

    查分和合并使用bsdiff主要依赖于bzip2这个框架


    然后我们就生成动态库文件(.so),因为安卓运行在linux平台所以生成的动态库文件和前面不同。自此我们生成了两个动态库文件,分别运行在服务端进行差分和运行在客户端进行合并。

    System.getProperty("os.name")能输出系统名字,能用于判断运行的操作系统

    System.load("路径")加载指定文件下的库文件

    差分算法:比较新旧文件,标识出翔通部分,压缩不同部分和额外部分(主要以来bzip2)生成patch文件

    合并算法:生成的patch文件和旧文件合成新文件

    当然这些算法内部实现非常复杂


    展开全文
  • 热更新总结--冷启动热更新

    千次阅读 2018-12-19 17:09:47
    本文章主要根据阿里出的《深入探索Android修复技术原理》后的个人总结   一、为什么直接补丁类直接导入到补丁包中,运行类加载时会产生异常并退出? 首先,因为dex加载到本地内存时,如果不存在odex文件,那么...

    本文章主要根据阿里出的《深入探索Android热修复技术原理》后的个人总结

     

    一、为什么直接补丁类直接导入到补丁包中,运行类加载时会产生异常并退出?

    首先,因为dex加载到本地内存时,如果不存在odex文件,那么首先会执行dexopt,其中

    if(doVerify){

        if(dvmVerifyClass(class)){

            ((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISPREVERIFIED;

            verified = true;

        }

    }

    dvmVerifyClass方法如果APK只存在一个dex文件,直接返回true,这时类会被打上CLASS_ISPREVERIFIED标记。

    在运行时,由于补丁类跟原类不在同一个dex文件中,此时原类已经被打上了ISPREVERIFIED标记,由于此时在不同的dex文件了,就会直接抛出dvmThrowIllegalAccessError异常。

     

    解决方案:

    创建一个新的无关的帮助类放到一个单独的dex文件中,原dex中所有的构造函数都引用这个类,一般方案是侵入dex打包流程,利用.class字节码修改技术,在所有.class文件的构造函数中引用这个类,插桩由此而来。这样做在dexopt时,类将不会被加上CLASS_ISPREVERIFIED标志。因此运行时就不会有这个异常了。

     

    但是如此的话,本来只需要DEX第一次加载到内存时校验一次即可,但是因为未加CLASS_ISPREVERIFIEDQ标记,那么类的校验和优化都将在类的初始化阶段进行。

     

    虽然每次加载加一个类时间的是很快的,但是每次APP启动的时候会加载很多类,就会造成短暂白屏,这对用户来说无法容忍。


    Dalvik虚拟机加载一个类,有三部ResolveClass,LinkClass,InitClass。

    Link:父类或实现接口权限控制检查主要发生在Link阶段。

    Init:这个方法主要完成父类的初始化、当前类的初始化、静态变量的初始化赋值等操作。


    QFix方案:

        手Q热修复方案巧妙的躲过了插桩方案,躲过原理如下:

        首先类加载时由于调用类已经打上了CLASS_ISPREVERIFIED标记,所以会被检查,检查时又由于不在同一个DEX文件而直接报错,所以这时如果能让两个类的pDvmDex相同,那么就可以绕过这个判断进行demDexSetResolvedClass()将类放入到虚拟机。 

            如果将补丁类放到原有的dex的pRexClasses数组中?

            

    1、具体实现,首先通过补丁工具反编译dex为smali文件拿到对应文件:

            preResolveClz:需要打包的补丁类(类A)的描述符,非必需,为了调试方便加上的这个参数。

            refererClz:需要打包的补丁类(类A)所在的dex文件的任何一个类描述符。只要是与该补丁类在同一个DEX文件中即可,所以拿了DEX文件中的第一个类。

            classIdx:需要打包的类A在原有dex文件中的类索引ID,如2425

    2、通过dlopen拿到libdvm.so库的句柄,通过dlsym拿到so库的dvmResolveClass/dvmFindLoadedClass函数指针。

    3、预加载引用类:android/support/annotation/AnimRes,这个AnimRes就是之前说的refererClz即当前dex文件的第一个类,这样dvmFindLoadedClass(“android/support/annotation/AnimRes”)返回值才不为null,然后dvmFindLoadedClass执行结果ClassObject作为第一个参数执行dvmResolveClass(AnimRes,2425,true)即可。

     

    第三个参数fromUnverifiedConstance,如果是true直接绕过检查,直接将类加载到虚拟机中。

    4、真正载入类时,由于之前已经把类加载到了虚拟机中,所以下一次在虚拟机中获取resClass时就会直接返回class对象而不再进行判断。

    5、JNI实现方案就是获取到原调用类的对象,然后原类的id,直接调用dvmResolveClass就可以了。                  

    *其中AnimRes,2425都是原DEX中的。书本第83页有源代码

     

    这样其实就绕过了加载时的dex判断,但是这个有个天然的缺陷就是,由于是在dexopt后绕过的,dexopt会改变原有的很多逻辑,许多odex层面的优化会固定字段和方法的访问偏移,这就会导致比较严重的BUG。

     


    以上两个方法都是为了绕过是否在同一个Dex的判断,而这个进行校验的判断条件就是:

    if(!fromUnverifiedConstance && IS_CLASS_FLAG_SET(referrer,CLASS_ISPREVERIFIED))

    插装法是为了给第二个条件变为false,手Q是给第一个条件变为false。

    但是手Q仅仅是把第三个参数变成true然后改变第一个条件变成false的话,还是没有把类加载到虚拟机中,下一次也就是真正加载类的时候还是获取不到类,导致还是要失败,所以还需要所需要dex指针及类对应dex中的id,进行加载。

     

    建议看一下书的第83页。


    Dalvik虚拟机加载dex文件,只会加载classes.dex文件,之外的dex文件会被直接忽略。所以补丁类需要跟现有dex合并为一个classes.dex文件。

     

    Art虚拟机则会先加载classes.dex文件,然后依次加载其他的dex文件,并且后续出现在其他dex文件中的“补丁类”将不再会被加载。所以Art下的方案:把最新的补丁类dex命名为classes.dex,原dex依次命名为classes(2、3、4…).dex就可以了,然后一起打包为一个压缩文件。再通过DexFile.loadDex得到DexFile对象,最后用该DexFile对象整体替换旧的dexElements数组就可以了。


    DexFile.loadDex尝试把一个DEX文件加载到内存中,在加载到native内存之前,如果dex不存在对应的odex,那么Dalvik虚拟机会进行dexopt,Art虚拟机会进行dexoat,最后得到的都是一个优化后的odex。实际上最后虚拟机执行的是这个odex而不是dex。

     

    因为我们执行的是odex文件,需要编译并加载dex到内存,对于dalvik虚拟机来说,因为最后加载的是一个合成完的dex文件,所以影响不大,因为总是要加载的。但是对于Art虚拟机来说,问题就很大,因为新的loadDex是补丁dex和Apk中原dex合并成一个完整补丁压缩包,所以dexoat会非常耗时。所以如果优化后的odex文件没生成或者不完整,那么loadDex便不能在应用启动的时候进行,因为会阻塞loadDex线程,一般是主线程。为了解决这个加载慢的问题,Sophix提供的方案是:把loadDex当做一个事务,如果中途被打断,那么就删除odex文件,重启的时候如果发现存在odex文件,loadDex完之后,反射注入/替换dexElements数组,实现打包。如果不存在odex文件,那么重启另一个子线程loadDex,重启后生效。

     

     

    • 在Dalvik下用Sophix自行研发的全量dex方案。

    • 在Art下本质上虚拟机已经支持多dex的加载,所以做的仅仅是把补丁dex作为主dex(classes.dex)加载而已


    QFix 多态问题 

    (避免插桩的方案,将新类提前加载到旧类pRexClasses中,并且用旧类的相关信息绕过检查)

     A b = new B();执行会尝试加载类B,方法调用链dvmResolveClass -> dvmLinkClass -> createVtable,此时为类B创建一个Vtable,其实在虚拟机中加载每个类都会为这个类生成一个Vtable表,vtable表就是当前类的所有virtual方法的一个数组,当前类和所有继承父类的public/protect/default方法就是virtual方法,因为public/protect/default修饰的方法都是可以被继承的。private/static方法不在这个范畴,因为不能被继承。

     

    子类vtable的大小等于子类vtable方法数+父类vtable的大小。

     createVtable主要进行了一下操作:

    1、判断是否有父类,如果有则将父类的vtable给子类的vtable

    2、遍历子类方法,判断是否方法名和参数都一致,如果一致证明是子类已经重写父类方法,那么替换vtable对应索引,子类方法覆盖掉vtable中父类的方法

    3、如果方法原型不一致则直接将方法插入到vtable的末尾

    4、如果不存在父类,则直接将在子类方法到vtable

     

    但是对于静态方法或者字段来说,b.name输出的是A方法也就是父类方法的name字段的内容,这就是因为 field/static是从当前引用类型而不是实际类型中去查找,如果找不到,再去父类中递归查找。

     

    问题出现原因:

            在optimize(dvmopt)阶段,会优化方法执行,会将virtual方法优化为OP_INVOKE_VIRTUAL_QUICK指令去执行而不是INVOKE_VIRTUAL,这个指令后面的立即数就是方法对应的索引。invoke-virtual-quick比invoke-virtual效率更高,直接从实际类型的vtable中获取方法索引,而省略了dvmResolveMethod从变量的引用类型获取该方法在vtable索引ID的步骤,所以效率更高。

            然而由于绕过了检查,在打包前类A的vtable值是vtable[0]=a_t2,而由于新增了a_ti方法,类A中vtable的值变成vtable[0]=a_t1,但是obj.a_t2()这行代码在odex中的指令其实是invoke-virtual-quick A.vtable[0],所以打包前用的是a_t2打包后用的是a_t1,这就导致了方法错乱。这都是多个dex的情况。

            这样就只能寄希望于在Dalvik虚拟机上的mergeDex方案,但是这个方案需要再dalvik虚拟机上进行,如果65535方法限制时,会造成内存爆炸,很可能导致更新失败。


    总结:Dalvik:

    QQ空间和手Q的热修复方案:

    • QQ空间的处理方式是在每个类中插入一个来自其他Dex的hack.class,由此让所有类都不满足在同一个dex中的条件从而无法pre-verified,因此不会打上IS_PREVERIFIED标签(缺点效率太低,每次都会去加载类,并且侵入打包流程,APP打开时可能遇到白屏)

    • Tinker的方式是合成全量的dex文件,这样所有类都在全量dex中解决,从而消除类重复而带来的冲突。(粒度太细,指令合成,性价比比较低)

    • QFix的方式是获取虚拟机底层函数,提前解析所有补丁类。以此绕过pre-verify检查。(需要获取底层虚拟机的函数也就是提前加载class到原pClassDex栈中的函数,不够稳定可靠。并且与QQ空间方案一样无法增加public函数)


    Sophix的方案

    目的在解析这个dex的时候找不到这个类的定义。因此,只需要移除定义的入口,对于类的具体内容不进行删除,这样可以最大限度的减少offset的修改。

     

    verifyAndOptimizeClasses中通过dexGetClassDef获取classDef(类的定义),

    dexGetClassDef则是通过pDexFile->pClassDex[idx]获取的, 其中pClassDex则是由pHeader->classDefsOff偏移处开始的,一个dex里面一共有pHeader->classesDefsSiz个类定义。因此只需要把header的从classDefsOff偏移开始的classesDefsSiz个classDef,删除其中想要替换的类名就可以了。

    然后修改pHeader->classesDefsSiz为修改后的数量即可。

     

    到此为止我的理解,最终该方案虽然是合成了dex文件,但是做法只是删除了header文件中的classDefs的类的引用,这样让机器自动去查找class2 class3 等dex,自动查找功能则是由Android原生的multi-dex的实现来实现的(multi-dex是把一个APK里面用到的所有类分到classes.dex classes2.dex classes3.dex等之中,而每个dex都只包含了部分的类定义,但单个dex也是可以加载的,因为只要把所有dex都加载进去,本dex不存在的类就可以在运行期间在其他的dex中找到)

     


    对于Application的处理

    问题,由于Application是整个APP的入口,因此在进入到替换的完成dex之前就会先通过Application代码呢,因为application只能在原dex文件中,因此在加载当前dex中的application时,未加载新的dex文件,此时application类会被打上IS_PREVERIFIED标志,当真正运行时,由于新dex中的class替换旧dex,导致不在同一个dex文件中,检验失败,抛出异常。

     

    解决方式:

    sophix:直接将Application的pre-verify标志删除掉。但最后发现这样做存在不可避免的问题,因此改成了如下所说的流程,类似于Amigo的流程,只是不通过gradle插件而是让开发者自行替换。

    tinker:让用户使用TinkerApplication注册到manifest中,真正的application则是当做参数传递给TinkerApplication,这样做是将用户的代码和真正的application分离开,这样入口application事先加载并不会影响真正的application,真正的application会被TinkerApplication根据生命周期通过反射调用。真正的Application会跟其他类相同的方式加载。唯一缺点就是需要对原有Application进行一定的修改。

    Amigo(美团修复方案):跟Tinker的方案类似,利用自定的gradle插件将APP的Application替换成Amigo自己的另一个Application,然后等该修复的都修复完之后,再将原Application加载回来,开发者无感知。

     

    Sophix将Application的preVerified删除掉,类加载的时候就会在执行ResolveClass,之前说过ResolveClass会对当前类doVerify操作,会对每一个指令进行校验,加载没有加载过的类也就会调用,也就是Resolve各个使用到的类,又由于Application是入口,在Application加载前,补丁dex文件中的类都未被加载,此时Resolve的各个类都是原Dex中的。接下在当补丁加载完成后,这些已经加载的类用到新dex的类时,由于也被打了pre-verified标签所以判断不在同一个dex中就会报错。

     

    解决该问题有两个方案:

    1、让Application用到的所有非系统类都和Application位于同一个dex里,这就可以保证pre-verified标志被打上,避免进入dvmOptResolveClass,而在加载完成后,再清除pre-verified标志,是的接下来使用其他类也不会报错。

    2、把Application里面除了热修复框架代码以外的其他代码都剥离开,单独提出放在一个其他类里面,这样是的Application不会直接用到过多的非系统类,这样,保证每个单独拿出来的类都和Application处于同一个dex的概率还是比较大的,如果想要更保险,Application可以采用反射的方式访问这个单独的类,这样就彻底把Application和其他的类隔绝开了。

     

    第一种方法比较简单,因为Android官方multi-dex机制会自动将Application用到的类都打包到主dex中,所以只要把热修复初始化放在attachBaseContext前面,一般都没问题。第二种方法稍加繁琐,实在代码架构层面进行重新设计,不过可以一劳永逸的解决问题。

     

    App的真是启动顺序:

    Application.attachBaseContext -> ContentProvider.onCreate -> Application.onCreate -> Activity.onCreate

     

    因此为了保证热更新程序运行之前尽量少的有类加载那么就需要放在App.attachBaseContext中,但是attachBaseContext可能还没有权限认证,因此有可能网络不可用,所以不能在这里进行热更新下载等。

     

    但是即使全部初始化都正确,仍然会有问题,在最终执行的时候,大多数时候都是以oat机器码的方式执行的,这里当引用localStorageUtil的init方法时,如果是根据method id直接从dex相关数据中获取ArtMethod结构然后执行,是没问题的。

     

    但是如果oat文件做过比较大的优化(见多态的影响),变为直接从localStorageUtil对象的所属类LocalStorageUtil里面去查找init方法,这个时候可能就有问题。这时LocalStorageUtil的类的方法结构发生变化即方法索引变化了,添加或减少了方法并对该方法有了影响,就会造成问题。

     

    正是由于这种限制,Sophix后面引用了一种新的更稳健的初始化方式,保证初始化在单独的入口类中进行,后面再用反射的方式替换为原有的Application,采用新的初始化方式。即:SophixApplication替代入口,真正实现逻辑通过反射调用,这样加载时不会加载其他非系统类。

     

    最后其实跟Amigo的做法相同,只是把入口类暴露给了开发者,对于开发者更透明。


    总结:

    Dalvik:合成一个dex,做法是将原有的dex头文件head文件中注册的需要更新的类给剔除掉,只是剔除了注册信息,让dex查找的时候找不到,与multi-dex方式类似,方法会自动去其他的dex文件中找,以实现热更新替换方案。这可以避免两个dex合成一个dex时,方法数超过65535时内存爆炸的情况,也可以正常让dex最大可能的使用dvmopt优化,并且不受多态的影响(见书99页,多态对QFix的影响)

    见书本105页。

    art虚拟机:直接将新的dex文件替换名字为classes.dex,原dex文件依次命名为classes2 3 4.dex,由于Art虚拟机的dex加载方式,自动实现热更新。

     

    对于Application的实现,最终方案还是替换Application,用代理Application来桥接真正的Application,利用反射调用真正Application的声明周期。

    展开全文
  • 热更新

    2019-06-11 23:39:54
    组件化 就是将app分成多个模板,每个模块都是一个组件(Model),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一成一个apk,这就是组件化开发。...
  • 热更新 热重载

    2019-09-02 15:18:06
    热更新 热更新:浏览器的无刷新更新,允许在运行时替换,添加,删除各种模块,而无需进行完全刷新重新加载整个页面 目的:加快开发速度,所以只适用于开发环境下使用 思路: 保留在完全重新加载页面时丢失的应用...
  • 热更新你都知道哪一些?本篇博客介绍热更新基本原理和作用,以及几种流行热更新的框架对比和优劣,快来看看吧。
  • App热更新 目前市面上成熟的商业热更新方案不少,有腾讯Bugly的Tinker封装,有阿里云的Sophix,也有游戏垂直行业的卓盟乐变。这些成熟方案,都有一个适用范围,即对App、对游戏整包进行热更新。前两者是和包名绑定...
  • 一文搞定Java热更新

    千次阅读 2019-09-26 09:24:30
    Java热更新 在持续交付的时代,重新部署一个新的版本只需要点击一下按钮。但在有的情况下,重新部署过程可能比较复杂,停机是不被允许的。所以JVM提供了另外一种选择:在不重启应用的前提下进行小幅改动,又称...
  • Android热更新研究与实现

    万次阅读 2019-07-11 17:35:59
    第一部分重点是将当下热门的热更新方案实现之后再研究,第二部分则是自己动手实现一个自己的热更新框架。 Android热更新技术的研究与实现之研究篇 ———概念讲解——– 热更新 相关概念 这个词出现的时间已经很...
  • Android热更新技术的研究与实现(一)

    万次阅读 多人点赞 2017-10-18 09:28:49
    第一部分重点是将当下热门的热更新方案实现之后再研究,第二部分则是自己动手实现一个自己的热更新框架。Android热更新技术的研究与实现之研究篇———概念讲解——–热更新 相关概念这个词出现的时间已经很久了,...
  • 科大讯飞和悦跑圈均表示,下架与“热更新”相关。然而,这并不是苹果应用商店第一次因为“热更新”而作出如此大规模的动作。不过,此次多款知名应用遭遇突然下架,也体现出苹果对其封闭生态系统的强力维护。数据显示...
  • React Native 实现热部署、差异化增量热更新

    万次阅读 热门讨论 2017-02-14 11:37:04
    项目已开源到github,链接...本篇博客同样是React Native中比较经典的内容:热更新部署。 Android原生App中我们实现热修复有很多种选择:Tinker、hotFix、Qzone的热更新等等。基本的思路都是大同小异的。React Nati...
  • Android 热更新框架

    万次阅读 2017-04-11 19:39:03
    大部分人都称为热更新,其实同热修复是一个意思。 本文转载:http://dev.qq.com/topic/57a31921ac3a1fb613dd40f3 Android 不仅系统版本众多,机型众多,而且各个市场都各有各的政策和审核速度,每次发布一个版本...
  • cocoscreator热更新

    千次阅读 2019-06-27 19:38:54
    首先我们要熟悉一下官方的热更新文档,了解热更新机制。其次自己要去通过实践来熟悉一下。 官方实例几处需要注意的,会导致热更新出问题 参考这篇博客 热更新目录的储存 /** * 这里需要注意的是,不要将相同的...
  • [Unity XLua]热更新XLua入门(一)-基础篇

    万次阅读 多人点赞 2017-01-14 12:26:03
    Aladdin_XLua前言前段时间腾讯开源了一个内部更框架XLua在Unity开发群里引起一阵热议,也受到广大开发者的捧,然后我当然也抱着好奇的心去学习学习。后面也会将扩展之后的工程放在git上,大家一起学习交流!在此...
  • ILRuntime Unity热更新

    万次阅读 热门讨论 2019-05-28 19:35:44
    在新的项目中,使用到了ILRuntime的热更新方式,不同于XLua等,这种方式的热更新是由纯C#实现的,所以就不需要客户端懂Lua的代码。更详细的介绍可以看官方的文档。 官方的介绍及文档为:...
  • creator js热更新

    千次阅读 2017-10-14 16:13:46
    creator 热更新方法: 大致原理:  creator 打包出来的工程目录和vs项目的目录基本相同,在客户端实际执行的文件其实就是build目录下的src和res文件,游戏中只要动态的改变src res 内容即可...
  • Flutter 应用热更新

    千次阅读 2020-04-24 11:33:00
    Flutter 热更新简介 所谓热更新,指的是当应用代码出现缺陷问题时,不需要重新打包提交App Store即可完成缺陷的修复。众所周知,使用原生技术开发的应用体验虽然好,但开发、上线周期长也常常被诟病,特别是当应用...
  • 游戏中热更新机制

    千次阅读 2018-03-19 10:49:24
    --------------------------------------------------------------------- 对一个游戏来说,无论是client或server都非常需要一套代码热更新的机制。它能大大提高开发效率,又能超乎玩家期望地在运营期在线修正bug和...

空空如也

1 2 3 4 5 ... 20
收藏数 140,901
精华内容 56,360
关键字:

热更新