QFix 探索之路 —— 手Q热补丁轻量级方案
导语
QFix 是手Q团队近期推出的一种新的 Android 热补丁方案,在不影响 App 运行时性能(无需插桩去 preverify)的前提下有效地规避了 dalvik 下”unexpected DEX”的异常,而且还是很轻量级的实现:只需调用一个很简单的方法就能办到。
热补丁方案及手Q上的使用
自2015年 Android 热补丁技术开始出现,之后各种方案和框架层出不穷,原创性的技术方案主要有以下几种:

性能无法提升,需要改变
插桩的解决方案会影响到运行时性能的原因在于:app 内的所有类都预埋引用一个独立 dex 的空类,导致安装 dexopt 阶段的 preverify 失败,运行时将再次 verify+optimize。近期我们通过 ReDex 尝试优化手Q的启动性能时发现:
- 保留手Q现有的插桩,启动性能没有任何优化效果;
- 去掉插桩,优化手Q启动相关类的 dex 分布,启动性能提升 30%。
另外即使后期手Q的发布版本实际上无需发布补丁,我们也需要预埋插桩的逻辑,这本身也是不合理的一点,所以确实有必要去探索新的方向,既保留补丁的能力,同时去掉插桩带来的负面影响。
重新分析”unexpected DEX”异常
寻找新的解决方案,还是需要回过头来分析下这个异常出现的条件:


可以看出,Qzone 的插桩方案是突破了条件2的限制(统一去掉了所有引用类的 preverify 标志),而微信 Tinker 的 dex 增量合成方案是突破了条件3的限制(将补丁和 app dex 合成后替换,原先 app 里在同一个 dex 的两个类,其中一个后来打在补丁里,合成后还是会在同一个 dex里),那有没有办法从条件1入手呢?条件1中 fromUnverifiedConstant 为 true 就行,其实之前就有从这个条件进行突破的方案:
主要思路是:每当系统调用到这个方法,通过 native hook 拦截这个系统方法,更改这个方法的入口参数,将 fromUnverifiedConstant 统一改为 true,但和 Andfix 类似,native hook 方式存在各种兼容性和稳定性问题,而且拦截的是一个涉及 dalvik 基础功能同时调用很频繁的方法,无疑风险会大很多。
找到新的“大陆”
这段逻辑所在的方法是 dvmResolveClass,通过类之间的引用会调用这个方法,入口参数分别是引用类的 ClassObject,被引用类的 classIdx,以及引用关联的 dalvik 指令是否为 const-class/instance-of,返回的是被引用类的 ClassObject,经反复阅读分析,终于发现了一个可以利用的细节:


dvmResolveClass 在最开始会优先从当前 dex 已解析类的缓存里找被引用类,找到了直接返回,找不到时说明被引用类还没有被加载,接着加载成功后,会往当前 dex 缓存里设置上这个类的引用,后续所有对补丁类的解析引用都不会走到后面的“unexpected DEX”异常逻辑里,至于 dex 里已解析类 get/set 的相关逻辑如下:



结合以上分析,我想到一个思路:只需首次引用到补丁类时能够成功突破上述三个条件之一的限制即可,Qzone 突破条件2和 Tinker 突破条件3的方法操作过重,而且带来的影响是持续性的,而从条件1入手很简单:补丁安装后,预先以 const-class/instance-of 方式主动引用补丁类,这次引用会触发加载补丁类并将引用放入 dex 的已解析类缓存里,后续 app 实际业务逻辑引用到补丁类时,直接从已解析缓存里就能取到,这样很简单地就绕开了“unexpected DEX”异常,而且这里只是很简单地执行了一条轻量级的语句,并没有其它额外的影响。
另外考虑多 dex 的情况,补丁类很可能被多个不同 dex 里的类引用,那么需要在每个 dex 里找到一个引用类来预先引用补丁类吗?如果 app 里引用类和补丁类原本是在同一个 dex 里,引用类有可能是 preverify 的,这种情况是需要预先引用的;如果原本就不是一个 dex 里的,引用类由于有对其它 dex 类的依赖,就肯定不是 preverify 的,这种情况条件2本来就是不满足的,就没有必要预先引用了,所以可以推断出只需要针对补丁类在原先 App 所对应的 dex 进行预先引用即可。
梳理了思路后,马上在一个简单的 demo 上验证:

demo 里补丁包含的类是 BugObject,通过对比,如果代码不包含上图红框里的预先引用的逻辑,出现了预期的“unexpected DEX”异常,如果加上这一行代码,demo 运行正常,而且补丁的修复功能也生效。通过 dexdump 查看,确实是优先通过 const-class 指令引用补丁类的。

没那么简单,初步方案行不通
上面的 demo 预埋了补丁里包含的类,但在实际运用中我们是无法预先设定哪些类要打补丁的,dex 里对补丁类 const-class/instance-of 方式的引用指令是编译时确定的,但具体是哪些类又需要在运行时动态确定,所以这种动态方式行不通,最初想到的是类似插桩的做法,预先把 app 里所有类都以 const-class 方式引用一遍,但很明显有以下问题:
1)由于 App 里类的数量很多,所有类的预先引用统一放在一个地方肯定不现实,需要分散在多个区,只对补丁类所在的少数几个区执行预先引用的操作,但这里如何划分的粒度不好把握,而且 App 里的类及数量一直变化,我们做过一些尝试,但没有比较理想的可考量的方案。
2)预先引用解析所有类,会增加引用类的加载耗时和引用语句本身的执行耗时,对于执行耗时,可以通过添加条件判断来优化,如果要解析的类在补丁类名列表里就执行该语句,否则就不执行,对于加载耗时,初步的测试结果如下(这里一个划分的区包含500个左右的类,并进一步区分了是否 preverify,而测试的补丁包里包含2个类):

从测试数据看,加载的耗时较长,而且补丁类不可预期,如果不巧分布在多个区里,累计耗时的影响将会严重得多。
3)该方案实现起来特别繁琐,不实用。
确定最终方案
新的方案在 Java 层找不到可行的实现方式,就尝试从 native 层切入,只需首次引用解析补丁类时,直接通过 jni 调用 dalvik 的 dvmResolveClass 这个方法,当然传入的参数 fromUnverifiedConstant 需要设为 true,这个思路与前面说的 native hook 方式不同,不会去 hook 这个系统方法,而是从 native 层直接调用:
- dvmResolveClass 方法是在 dalvik 的系统库 /system/lib/libdvm.so 里,通过 dlopen 即可获取该系统库的句柄
- 通过 dlsym 获取 dvmResolveClass 这个方法的地址
- 设定 dvmResolveClass 这个方法的三个入口参数,再调用 dvmResolveClass:1)引用类 referrer 的 ClassObject:这里需要设定一个引用类,并且能够获取到该类的 ClassObject;
2)补丁类的 classIdx:需要获取补丁类在 app 原先所在 dex 的 classIdx,通过这个 classIdx 可以在 dex 里找到已解析的类或者获取类的名字;
3)布尔值 fromUnverifiedConstant:在C/C++层,这个值可以固定设置为1或者 true。
这里的关键是能获取到前两个参数的值,第一个参数引用类的 ClassObject,最初借鉴的是 dvmResolveClass 里调用的 dvmFindClassNoInit 这个方法,但这个方法获取一个类的 ClassObject 需要两个参数,其中类名很容易构造,但需要额外的操作获取引用类的 ClassLoader 对象的地址,之后又找到一个更便利的方法 dvmFindLoadedClass:

这个方法只用传入类的描述符即可,但必须是已经加载成功的类,在补丁注入成功后,在每个 dex 里找一个固定的已经加载成功的引用类并不难。对于主dex,直接用 XXXApplication 类就行,对于其它分 dex,手Q的分 dex 方案有这样的逻辑:每当一个分 dex 完成注入,手Q都会尝试加载该 dex 里的一个固定空类来验证分 dex 是否注入成功了,所以这个固定的空类可以作为补丁的引用类使用。第二个参数 classIdx,可以通过 dexdump -h 获取:

这个过程可以通过一个小程序自动进行:
输入: 原有 apk 的所有 dex、补丁包所有的类名
输出: 补丁包每个类所在 dex 的编号以及 classIdx 的值
注1: 如果在补丁新增原 app 不存在的类,运行时新增类只会被补丁 dex 即同一个 dex 里的类所引用,所以新增的补丁类无需预先解析引用。
注2: 由于”unexpected DEX”异常出现在 dalvik 的实现里,art 模式下不会存在,以上预先引用补丁类的逻辑只需用在5.0以下的系统。
最终新方案的整体实现流程如下图所示:

可以看出,新的方案是很轻量级的实现,只需一个很简单的 jni 方法调用就能解决问题,既不用构建时预先插桩去 preverify,也不用下载补丁后进行 dex 的全量合成。
兼容性问题及解决
这个方案由于是 native 层的,我们也通过众测方式对兼容性做了充分的验证:
1. 不同系统版本导出符号:
在2.x版本dalvik是用C写的,2.3以上的4.x版本是用C++写的,基于C++ name mangling原理, dvmFindLoadedClass在编译后会变为_Z18dvmFindLoadedClassPKc,但经IDA反汇编libdvm.so分析,dvmResolveClass没有变化
2. yunos ROM的兼容性问题:
在第一次众测任务中,有446位用户参与,其中有6位反馈补丁不生效的问题,从反馈的结果码看都是libdvm.so加载成功,但是符号导出为NULL导致的,后来发现这6位用户安装的都是yunos的rom,经分析定位到原因如下:
可以看到dlopen libdvm.so时将库的名字改为了libvmkid_lemur.so,yunos的dalvik实现实际上在后面这个库里,而且通过反汇编发现导出的符号名也变化了,但内部的实现逻辑没有变化:
1 2 |
dvmResolveClass -> vResolveClass _Z18dvmFindLoadedClassPKc -> _Z18kvmFindLoadedClassPKc |
在dlsym调用时考虑以上两种可能的符号名即可,经本地和以上问题用户的再次验证,已成功解决。
3. x86平台的兼容性问题:
解决了yunos的兼容问题后,在第二次众测任务中,有1884位用户参与,有3位反馈异常,发现问题用户都是x86平台的,由于最开始未对x86平台作兼容,arm平台的动态库在x86手机上运行的异常有两种:
a) 部分手机一直卡在黑屏界面,经日志定位,这些手机都安装了houndini的第三方库,会自动将arm的so转换为x86平台兼容的,so加载及符号导出都没问题,在成功获取dvmResolveClass符号地址后,就一直卡在dvmResolveClass的调用逻辑里,应该是houndini库的转换问题
b) 部分手机运行正常,但导出符号都为NULL
在提供x86平台的so后,以上两个问题也成功解决了。
结语
本文探讨的主要是为解决补丁 Java 方案在 dalvik 下”unexpected DEX”异常提供一个新的思路,在整个 Android 补丁大的技术框架下,只是其中一个环节,有问题,欢迎大家多多交流!
参考链接
Android上实现可执行的SO文件
Linux
下的so
文件通常是作为动态链接库使用的,但其实so
文件跟可执行程序一样都是ELF
格式,所以应该都是可以直接执行的。
Linux
下编译可以执行的so
文件如下:
1 2 3 4 5 6 7 8 |
#include <stdio.h> #include <stdlib.h> void lib_entry() { printf("Entry point of the service library\n"); exit(0); } |
注意,lib_entry()
必须以exit(0)
结束,否则会导致进程退出失败。
使用如下命令编译源代码:
1 |
$ gcc -shared service.c -o libservice.so -Wl,-e,lib_entry -fPIC |
-Wl
表示传递给链接器ld
的参数,分隔的逗号会被替换成空格。-e,lib_entry
就指明了入口函数。
而对于Android
来说,只需要在Android.mk
文件中增加LOCAL_LDLIBS += -Wl,-e,lib_entry
就可以达到相同的目的了。
需要注意的问题
1.这个入口函数是否可以传递类型int main(int argc,char* argv[])
这样的参数进去?
答案是不能,那么启动参数从哪里读取呢? 答案就是从/proc/$pid/cmdline
中手工解析获取。
2.入口函数并没有初始化C
库的代码,在调用代码时候为什么没有崩溃?
正常情况下,可执行程序的入口函数实际上是C
库的的入口函数,然后C库自身初始化完成,解析参数后调用我们自己实现的入口函数。按照常规逻辑,如果没有初始化C
库,那么调用C
库函数的时候,几乎肯定是会崩溃的。
反编译正常的可执行程序,应该都能看到C库的初始化函数,而指定了入口的,基本上都没有这个函数的调用。
但是在Linux
下面ld.so
会帮我们初始化一次C
库,而我们又是被ld.so
加载起来的,因此理论上,我们不需要再次初始化C
库了。
参考链接
Android系统上解决SQLite数据库在断电时候丢失数据的问题
Android
系统上使用SQLite
数据库存储数据,结果发现,如果刚刚写入数据之后在很短的时间之内,如果立即断电会丢失刚刚写入的数据。
根据Google
官方的文档,发现,从API-16
开始,提供了enableWriteAheadLogging
这个API
来要求SQLite
先写日志,后写数据库。这个行为才是常规数据库默认的行为。
一般Android
设备使用的存储设备都是Flash
闪存,是有写入寿命以及空间限制的,因此默认不启用日志功能,也是迫不得已,更何况数据库日志属于只增不减的,这就导致长时间运行后,会出现空间无法释放的问题。
还有一个解决方法就是,插入以及修改数据的时候,启用SQLite
的事务模型,由于事务一定要保证数据已经同步到磁盘了,因此,可以避免出现断电后数据由于没有刷新到磁盘导致的数据丢失。
1 2 3 4 5 6 7 |
db.beginTransaction(); try { ... db.setTransactionSuccessful(); } finally { db.endTransaction(); } |
很多时候,会发现直接通过Kill
,杀掉进程,一般是不会丢失数据的,原因在于磁盘写入的时候,系统会进行缓存,等合并到一定的量或者时间,系统一次性同步到磁盘,这样可以大大提供系统的性能。因此进程虽然已经死掉了,但是系统还是会把已经提交到内核的数据刷新到磁盘的,因此表现就是数据不会丢失。但是如果是断电的话,系统也就无能为力了。于是表现就是,越是新的Linux
内核版本,反倒越是在异常断电的时候容易丢失数据。
参考链接
在阿里云的Ubuntu 14.04系统上解决Tomcat 7由于OOM(Out Of Memory)而被系统杀掉的问题
最近服务器上面一直出现Tomcat
莫名奇妙的被系统杀掉,后来从系统的日志中找到如下信息:
1 2 |
Oct 10 02:55:16 AY130422143404983ad9 kernel: [451276.905623] Out of memory: Kill process 809 (java) score 241 or sacrifice child Oct 10 02:55:16 AY130422143404983ad9 kernel: [451276.905684] Killed process 809 (java) total-vm:1291052kB, anon-rss:493732kB, file-rss:0kB |
原来是系统内存不足,导致进程被杀掉了,网上搜了一下,解决方法有两个
1.限制Tomcat
使用的内存
方法如下:
1 |
$ sudo vim /usr/share/tomcat7/bin/setenv.sh |
在文件尾部增加如下配置:
1 2 |
#防止Tomcat的OOM export JAVA_OPTS="-server -Xms512M -Xmx1024M" |
然后重启Tomcat
1 |
$ sudo service tomcat7 restart |
2.为阿里云服务器增加swap
分区/swap
文件,来解决物理内存不足的问题
阿里云的服务器默认没有开启交换分区,导致内存极易耗尽导致服务被杀死,解决方法就是手工增加一个交换文件,来解决这个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#如果要修改或者调整交换文件的大小,需要先停止交换文件,然后才能调整 # sudo swapoff -a #创建一个2G的文件,要求每个扇区512个字节 $ sudo dd if=/dev/zero of=/swaps_file bs=512 count=4194308 $ sudo chmod 0644 swaps_file $ sudo mkswap /swaps_file $ sudo swapon /swaps_file $ sudo cp /etc/sysctl.conf /etc/sysctl.conf.bak.old $ sudo sed -i 's/^vm.swappiness[ \t]*=[ \t]*0$/vm.swappiness = 60/g' /etc/sysctl.conf $ sudo cat /proc/sys/vm/swappiness $ sudo sysctl -w vm.swappiness=60 $ sudo cat /proc/sys/vm/swappiness $ sudo sed -i '$a\/swaps_file swap swap defaults,discard 0 0' /etc/fstab $ sudo cp /etc/rc.local /etc/rc.local.bak.old $ sudo sed -i 's/^swapoff[ \t]*-a$/swapon -a/g' /etc/rc.local |
参考链接
Linksys WRT 1900AC的V1,V2版本配置信息杂记
在淘宝上买了个Linksys WRT 1900AC
是没有外包装的,网上搜了一下,发现存在的V1,V2两个硬件配置版本。
V1的CPU是1.2G带风扇,V2是CPU 1.3G不带风扇,内存V2是512MB ,V1是256MB
详细配置信息如下图:
官方固件下载地址为:
http://www.linksys.com/us/support-article?articleNum=148550
目前最新的官方固件链接:
http://cache-www.belkin.com/support/dl/FW_WRT1900AC_1.1.10.167514_prod.img
本站缓存的Linksys WRT 1900AC V1
最新官方固件地址:
fw_wrt1900ac_1-1-10-167514_prod-img
OpenWRT的相关固件下载地址:
https://wiki.openwrt.org/toh/linksys/wrt1x00ac_series#stable
本站缓存的Linksys WRT 1900AC V1
的OpenWRT的目前版本的快照:
openwrt-15-05-1-mvebu-armada-xp-linksys-mamba-squashfs-factory-img
参考链接
Android中WebView中执行JavaScript获取页面的宽高都是0的问题
最近使用Android
中的WebView
开发应用,结果在实际使用的时候,前端开发的网页展现的时候,总是什么都看不到,结果跟踪分析发现,页面在加载完成后,就去获取页面的宽高,然后根据页面的宽高进行页面的渲染,但是页面加载完成后JavaScript
中得到的页面的宽高都是0,结果就导致整个的页面被渲染成了空页面。
项目是使用一个Dialog
里面嵌入WebView
的方式进行处理的,当网页加载完成之前,是不显示出来的,只有当网页加载完成后,页面才会展现出来。
实际的情况是,如果先展示Dialog
,等整个Dialog
已经完全展现出来之后,再去加载页面,这个时候,JavaScript
代码是可以获取到正确的宽高的。但是,如果先加载页面,然后等待页面的加载完成通知,再去显示页面的时候,这个时候,由于Dialog
还没有显示出来,此时内嵌的WebView
并没有得到实际的宽度高度,因此只能获得到0。
示例工程的代码如下:
布局文件layout_webview.xml
1 2 3 4 5 6 7 8 9 10 11 12 |
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <WebView android:id="@+id/web_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layerType="software" /> </FrameLayout> |
对话框代码WebViewDialog.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
import android.app.Dialog; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.view.Window; import android.view.WindowManager; import android.webkit.JavascriptInterface; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Toast; public class WebViewDialog extends Dialog { public WebViewDialog(Context context) { super(context); setContentView(R.layout.layout_webview); Window window = getWindow(); WindowManager.LayoutParams params = window.getAttributes(); params.type = WindowManager.LayoutParams.TYPE_TOAST; params.height = WindowManager.LayoutParams.MATCH_PARENT; params.width = WindowManager.LayoutParams.MATCH_PARENT; window.setAttributes(params); mWebView = (WebView) findViewById(R.id.web_view); mWebView.setWebViewClient(mWebViewClientBase); mWebView.getSettings().setJavaScriptEnabled(true); mWebView.addJavascriptInterface(new JsBridge(),"JsBridge"); } public void showWebView(){ final String strHtml = "<html>" + " <head>" + " <script>" + " window.JsBridge.jsGetWindowWithHeight(document.documentElement.clientWidth,document.documentElement.clientHeight);" + " </script>" + " </head>" + "</html>"; mWebView.loadData(strHtml,"text/html","UTF-8"); } public class JsBridge{ @JavascriptInterface public final void jsGetWindowWithHeight(int aWidth,int aHeight) { Log.d("jsGetWindowWithHeight","Width=" + aWidth + " Height=" + aHeight); Toast.makeText(getContext(),"Width=" + aWidth + " Height=" + aHeight,Toast.LENGTH_LONG).show(); } } private class WebViewClientBase extends WebViewClient { @Override public void onPageFinished(WebView view, String url) { mHandler.removeCallbacks(mShowRunnable); mHandler.post(mShowRunnable); } } private final Runnable mShowRunnable = new Runnable() { @Override public void run() { WebViewDialog.this.show(); } }; private final WebViewClientBase mWebViewClientBase = new WebViewClientBase(); private WebView mWebView; private static Handler mHandler = new Handler(Looper.getMainLooper()); } |
主界面代码MainActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import android.os.Bundle; import android.view.View; import android.widget.Button; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); webViewDialog = new WebViewDialog(this.getApplicationContext()); setContentView(R.layout.activity_main); Button btn = (Button)findViewById(R.id.btnClick); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { webViewDialog.showWebView(); } }); } private WebViewDialog webViewDialog; } |
主界面布局文件activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/btnClick" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Click" /> </RelativeLayout> |
完整运行整个例子,会发现最后输出的屏幕的宽高信息都是0。
这个问题发生的原因是由于界面在页面加载之后才会显示,由于页面还没有显示,因此从JavaScript
中获取页面的大小的时候,只能获取到0.
这个问题如果要求页面解决的话,那么编写网页的开发人员,只要监听window.onsize
事件即可。
如果要求WebView
的开发者来解决的话,则其解决方式如下:
1.修改WebViewDialog
初始化网页时候的策略,初始化时候禁止JavaScript
执行。WebViewDialog
构造函数中的mWebView.getSettings().setJavaScriptEnabled(true);
调整成mWebView.getSettings().setJavaScriptEnabled(false);
2.页面加载逻辑照旧执行。
3.实现Dialog
的onAttachedToWindow
函数,在这个函数中重新加载数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Override public void onAttachedToWindow() { /*此处我们呼应下面代码中禁用JavaScript的支持的部分代码 * 原因也已经解释的非常详细了 * 但是此处需要注意,就是先reload再次启用JavaScript这个顺序不要乱掉,否则 * 可能还没有调用reload之前,前一个页面已经执行了JavaScript导致页面上面的埋点两次执行。 * * 关于性能的隐忧,由于我们重新reload了页面,地址链接并没有改变,因此并不会去服务器上面重新获取页面 * 此处的性能隐忧,应该是不存在的 * * 至于是不是需要手工设置一下Chrome内核的缓存时间,这个在目前的实际实验观察看来,是不需要的。 * * */ mWebView.reload(); mWebView.getSettings().setJavaScriptEnabled(true); } |
最终的WebViewDialog.Java
的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
import android.app.Dialog; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.view.Window; import android.view.WindowManager; import android.webkit.JavascriptInterface; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Toast; public class WebViewDialog extends Dialog { public WebViewDialog(Context context) { super(context); setContentView(R.layout.layout_webview); Window window = getWindow(); WindowManager.LayoutParams params = window.getAttributes(); params.type = WindowManager.LayoutParams.TYPE_TOAST; params.height = WindowManager.LayoutParams.MATCH_PARENT; params.width = WindowManager.LayoutParams.MATCH_PARENT; window.setAttributes(params); mWebView = (WebView) findViewById(R.id.web_view); mWebView.setWebViewClient(mWebViewClientBase); /*此处禁用JavaScript,而是在后面onAttachedToWindow事件中再启用JavaScript的支持,同时强制页面刷新 * 这么处理的原因是由于前端同学在开发页面时候,会使用一个自有的JavaScript框架,这个框架会在页面 * 加载完成后,立即获取页面的宽高,但是此时获取到的宽高都是 0 ,因为此时Chrome还没有完成整个页面的Layout * 因此我们需要在页面完成Layout后再次加载页面才可以,但是如果此处启用JavaScript的支持会导致埋点数据的意外 * 上行,导致双份的埋点问题,因此,此处强制禁用JavaScript * */ mWebView.getSettings().setJavaScriptEnabled(false); mWebView.addJavascriptInterface(new JsBridge(),"JsBridge"); } @Override public void onAttachedToWindow() { /*此处我们呼应下面代码中禁用JavaScript的支持的部分代码 * 原因也已经解释的非常详细了 * 但是此处需要注意,就是先reload再次启用JavaScript这个顺序不要乱掉,否则 * 可能还没有调用reload之前,前一个页面已经执行了JavaScript导致页面上面的埋点两次执行。 * * 关于性能的隐忧,由于我们重新reload了页面,地址链接并没有改变,因此并不会去服务器上面重新获取页面 * 此处的性能隐忧,应该是不存在的 * * 至于是不是需要手工设置一下Chrome内核的缓存时间,这个在目前的实际实验观察看来,是不需要的。 * * */ mWebView.reload(); mWebView.getSettings().setJavaScriptEnabled(true); } public void showWebView(){ final String strHtml = "<html>" + " <head>" + " <script>" + " window.JsBridge.jsGetWindowWithHeight(document.documentElement.clientWidth,document.documentElement.clientHeight);" + " </script>" + " </head>" + "</html>"; mWebView.loadData(strHtml,"text/html","UTF-8"); } public class JsBridge{ @JavascriptInterface public final void jsGetWindowWithHeight(int aWidth,int aHeight) { Log.d("jsGetWindowWithHeight","Width=" + aWidth + " Height=" + aHeight); Toast.makeText(getContext(),"Width=" + aWidth + " Height=" + aHeight,Toast.LENGTH_LONG).show(); } } private class WebViewClientBase extends WebViewClient { @Override public void onPageFinished(WebView view, String url) { mHandler.removeCallbacks(mShowRunnable); mHandler.post(mShowRunnable); } } private final Runnable mShowRunnable = new Runnable() { @Override public void run() { WebViewDialog.this.show(); } }; private final WebViewClientBase mWebViewClientBase = new WebViewClientBase(); private WebView mWebView; private static Handler mHandler = new Handler(Looper.getMainLooper()); } |
Ubuntu 16.04出现“/tmp 容量不够”的提示,如何增大“/tmp”空间大小
Ubuntu 16.04
出现“/tmp 容量不够
”的提示,当时安装系统的时候/tmp
是单独划分出来的,大小被限制为2GB
,这个时候尝试用gparted
来进行拉大分区的时候,发现由于分区在其他分区中间,导致没办法加大临时分区。
当时划分分区的时候,系统分区的大小是足够的,因此,我们只需要在/etc/fstab
中注释掉临时分区即可,此时系统会自动在根分区"/
"下面生成临时文件即可了。
1 |
$ sudo vim /etc/fstab |
注释掉这句话即可:
1 2 |
# /tmp was on /dev/sdc6 during installation UUID=5c6f4b66-0d9c-4da2-9362-4da121fd4657 /tmp btrfs defaults |
之后需要重启系统。
在处理完成后,记得修改回原样。
PuTTY连接跳板机频繁被断开的问题
公司的服务器只能通过跳板机的方式来链接,因此在Windows
中使用PuTTY
连接跳板机,结果实际使用中发现,会频繁被跳板机断开,而如果在目标机器中执行一个ping 127.0.0.1
则连接始终不会被断开。那么说明是由于跳板机设置了一段时间如果没有报文通信就断开连接的功能,而且这个时间设置的比较激进。
请教了一下别人,找到如下设置即可解决这个问题:
其实质是开启了TCP
的KeepAlive
心跳报文。默认这个时间是"0",也就是TCP
链路中不发送心跳报文。把这个时间设置成"1
",也就是每秒都发送一次心跳。
Android Studio升级到2.2正式版本后,在Windows命令行中执行“gradlew build”时,报告错误“Unsupported major.minor version 52.0”
Android Studio
升级到2.2
正式版本后,在Android Studio
中编译一切正常。但是在Windows
命令行中执行“gradlew build
”时,报告如下错误:
1 2 3 4 5 6 7 8 |
* What went wrong: A problem occurred evaluating project ':XXX'. > java.lang.UnsupportedClassVersionError: com/android/build/gradle/LibraryPlugin : Unsupported major.minor version 52.0 * Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. |
这个原因,目前本人遇到的原因是,机器上同时安装了jdk1.7.0_80
,jdk1.8.0_73
两个版本的JDK
,而环境变量中的JAVA_HOME
指向的是jdk1.7.0_80
。
解决方法就是修改JAVA_HOME
指向jdk1.8.0_73
即可。
注意,修改完成环境变量后,需要重启一下Android Studio
,以及Windows
命令行窗口。否则环境变量不生效。