前言6月23日,微信团队发布如下通知,禁止小程序使用JavaScript解释动态更新代码。消息一出,小程序开发者一片哗然,一片哗然,甚至有人声称要开始加班改代码。2018年1月起写《荆棘:微信小程序也要强制热更新代码,鹅厂不收你来操我》(https://zhuanlan.zhihu.com/p/34191831)这文章开头,提出了在小程序中使用JavaScript解释器的趋势。然而四年过去了,微信小程序终于规定不再允许使用JavaScript解释器。小程序热更新时代结束了吗?当然不是,要是就这样过去了,就没有我的文章了。其实四年前写上一篇文章的时候,我就已经想好了解决办法,没想到一年后的微信小程序。刚开始屏蔽JavaScript解释器就让我原本设想的计划拖到了四年后的今天。n所以今天我们的文章主要讨论两点:如何突破微信小程序对JavaScript解释器使用的限制,实现代码热更新。为什么理论上不可能从根本上禁止小程序代码的热更新。基本步骤&最终效果示例代码Github仓库:https://github.com/bramblex/jsjs-vm-demo我们首先需要编写一个JavaScript编译器,将JavaScript代码编译成二进制字节码。找到一个图像,编码字节码并将其隐藏在图像中。在小程序中引入一张隐藏了JavaScript字节码的图片,并对字节码进行解码。编写相应的字节码虚拟机,执行从图片中提取的字节码。实现字节码虚拟机为什么要将JavaScript编译成字节码?我们的目的是为了绕过微信小程序的代码审查限制,所以我们要尽量隐藏两件事。第一个是想办法隐藏解释器,因为一个完整的JavaScript解释器代码量巨大,而且往往需要导入别人写的库,自己无法维护。这样的解释往往不需要什么高深的技术手段,只要字符串匹配上,就能查出七八个。例如,要在一个小程序中引入代码到一个完全可用的JavaScript解释器中,至少需要引入至少100k或更多的代码。这个目标太大了,几乎难以掩饰。但是在小程序中引入一个可以执行字节码的虚拟机实现只能引入10k左右的代码,压缩前的总代码量不超过千行代码,更容易隐藏动态代码的实现。比如我目前实现的字节码虚拟机,除了try-catch和with,能实现ES5所有能力的虚拟机一共只有7k,这个可以再压缩。第二点我们需要隐藏热更新的JavaScript代码不被微信发现。比如你在热更新中通过明文接口传输大量的JavaScript代码,只要微信稍微拦截一下你的网络,不就完全暴露了吗?所以,在将代码编译成二进制字节码后,微信没有办法简单的拦截你的接口请求,判断里面是否有JavaScript代码来判断你是否对代码进行了热更新。在没有清楚地了解二进制文件的格式之前,根本不可能挑出它到底是什么1,更何况二进制加密和混淆算法满街都是,行数也不多……下面是是我实现的字节码指令集,总共只有50多条指令:exportenumOpCode{NOP=0x00,UNDEF=0x01,NULL=0x02,OBJ=0x03,ARR=0x04,TRUE=0x05,FALSE=0x06,NUM=0x07,ADDR=0x08,STR=0x09,POP=0x0A,TOP=0x0D,TOP2=0x0E,VAR=0x10,LOAD=0x11,OUT=0x12,JUMP=0x20,JUMPIF=0x21,JUMPNOT=0x22,FUNC=0x30,CALL=0x31,NEW=0x32,RET=0x33,GET=0x40,SET=0x41,IN=0x43,DELETE=0x44,EQ=0x50,NEQ=0x51,SEQ=0x52,SNEQ=0x53,LT=0x54,LTE=0x55,GT=0x56,GTE=0x57,ADD=0x60,SUB=0x61,MUL=0x62,EXP=0x63,DIV=0x64,MOD=0x65,BNOT=0x70,BOR=0x71,BXOR=0x72,BAND=0x73,LSHIFT=0x73,RSHIFT=0x75,URSHIFT=0x76,OR=0x80,AND=0x81,否T=0x82,INSOF=0x90,TYPEOF=0x91,}下面是示例代码及其编译后的字节码:JavaScript代码与编译后的字节码相比,在这段字节码中还可以看到wxshowModal等字样。上面的字节码是以下指令的二进制表示(摘录):.main_1:STR(09)"wx"(007700780000)LOAD(11)TOP(0d)STR(09)"showModal"(00730068006f0077004d006f00640061006c0000)GET(40)ARR(04)TOP(0d)NUM(07)0(0000000000000000)OBJ(03)TOP(0d)STR(09)“标题”(007400690074006c00650000)STR(09)“这是图中隐藏一段代码”(8fd9662f4e006bb5969085cf572856fe72474e2d76844ee378010000)SET(41)POP(0a)TOP(0d)STR(09)"content"(0063006f006e00740065006e00740000)STR(09)"这是一段隐藏在图片中的代码"(8fd9662f4e006bb5969085cf572856fe72474e2d76844ee378010000)SET(41)POP(0a)TOP(0d)STR(09)"成功"(00730075006300630065007300730000)NULL(02)NUM(07)1(3ff0000000000000)ADDR(08).anonymous_2FUNC(30)SET(41)POP(0a)SET(41)POP(0a)CALL(31)POP(0a)RET(33)毕竟是demo。如果真的需要的话,还有很大的优化空间。比如字节码字面量现在很简单大致直接内联,如果把数据和代码部分分开,可以获得更好的性能。比如字符串的编码是utf16编码。如果转成utf8编码,可以节省空间等等。如果我有心情,我会稍后再做。隐藏图片中的字节码我们说过要隐藏虚拟机和热更新的代码,但是想想看,一个普通的小程序一天到晚需要往二进制文件里加。这种行为是不是很奇怪?是的,这个东西非常非常奇怪,因为一个普通的小程序根本不需要读写二进制文件。但是如果我告诉一个小程序,我需要用小程序的二维码做一张分享图片给用户保存,而且这张分享图片需要经常更新,这不是很合乎逻辑吗?所以我们需要在图片中隐藏热更新的字节码,伪装成正常的小程序行为,保证图片看起来正常。下面是我们一开始的例子中的图片。左图是原图,右图是隐藏了我们上面的示例代码后的图。只有仔细观察才能看出细微的差别。仔细查看隐藏字节码的区域。它与原始图片略有不同。图片的一个像素点有RGBA,一共四个字节。为了尽量减少图片的影响,我们选择只隐藏图片alpha中的字节码代码。Channel,这里使用最简单的编码方式,将RGBA中的A编码为bit。如果A高于0xF8,则为1,否则为0。编码和解码算法如下:编译器中的编码算法(左)和小程序中执行的解码算法(右)。在小程序中,只需要在Canvas上绘制图片,并一张一张读取Alpha通道上的数据即可。然后解码可以隐藏在图片中的字节码。最后,通过上一节我们实现的字节码虚拟机,我们就可以执行我们要热更新的代码了。为什么不能从根本上禁止小程序代码的热更新?先说结论,只要满足以下两个条件,从根本上禁止热更新是无稽之谈:宿主语言图灵完备性允许先通过网络读取数据,如果宿主语言是图灵完备的,那么宿主语言可以实现任何其他图灵完备的编程语言。比如JavaScript图灵完备,那么你可以用JavaScript实现JavaScript解释器、Python解释器、PHP解释器等,只要你能想到的编程语言解释器,你甚至可以设计自己的字节码比如本文虚拟机.所以公告一出,第一个楼下回复的朋友就把JavaScript解释器搞坏了,真是可笑。公告发布的第一天,就有小伙伴在评论区刷屏。其次,您可以将任何可以获取不同输入并产生不同结果的程序称为解释器。无非是其强大的表达能力。弱和弱的区别,是通用的还是专用的,所以这个界限很模糊。比如在我们的业务中,可能需要程序从服务器拉取一个配置。这个配置可能是某些功能的开关是否显示等等。那我拉一个配置文件和一个JavaScript代码动态执行有本质区别吗?其实你也可以理解为代码只是一个解释器/编译器的配置文件,并没有那么特殊,唯一不同的是代码设计一般,复杂。所以才有这么一句话,代码就是数据,数据就是代码。写在最后在文章的最后,向两位科学家致敬。第一位是艾伦·图灵,他提出了图灵机,奠定了计算理论的基础。第二个是奠定现代信息论基础的香农。感谢巨人为我们提供了肩膀。艾伦·图灵(左)克劳德·香农(右)
