Android Studio 2.3调试小米手机安装失败

Android Studio 2.3调试小米手机 MIUI 8.7.4的时候,安装 APK一直失败,错误信息如下:

网上查询了一下,是 MIUI自身的优化导致的问题。
解决方法就是在 MIUI-> 设置-> 更多设置-> 开发者选项-> 启用MIUI优化,关闭这个选项就可以了。

参考链接


Android Studio 2.3 adb install-multiple Failed to create session Failure [UNS...

macOS Sierra (10.12.3)使用Android Studio 2.2.3导入Android 5.1.1_r38 (Lollipop)源代码

1.编译 Android 5.1.1_r38源代码

参考链接:
macOS Sierra (10.12.3)上编译ARM版本Android 5.1.1_r38 (Lollipop)源代码

2.调整 Android Studio的属性

默认情况下, macOS的磁盘分区文件名是不区分大小写的,但是我们编译的 Android 5.1.1_r38源代码却是在新建的一个区分大小写的分区上的,因此需要临时要求 Android Studio区分文件名的大小写。

在其中增加

3. 编译 Android SDK

Android SDK我们需要自己编译注意,此时磁盘空间可能已经不足了,我们首先增大一下磁盘空间

由于我们修改了 TARGET_BUILD_TYPE,因此需要调整代码

全部替换里面的 ${OUT_DIR}/target${OUT_DIR}/debug/target

编译代码

编译好的文件在 out/host/darwin-x86/sdk/sdk目录下面。

4.生成工程文件

工程目录与编译目录相同

编译完成后,在 /Volumes/android目录下,生成 android.ipr,然后用 Android Studio 2.2.3导入已经存在的项目即可,导入后,需要比较长时间建立索引,要耐心等待。

导入完成后,根据错误提示,设置 JDK为我们编译内核时候的 JDK,设置 Android SDK为我们刚刚编译的版本即可。
File-> Project Structure-> Project Settings-> Project-> Project SDK中设置 JDK版本。

File-> Project Structure-> Project Settings-> Modules-> android-> Dependencies中设置 Android SDK路径以及版本。

参考链接


macOS Sierra (10.12.3)上编译ARM版本Android 5.1.1_r38 (Lollipop)源代码

前置条件


  • macOS Sierra  (10.12.3)
  • Homebrew (1.1.9 或更高版本)
  • Xcode (8.2.1 或更高版本)
  • Xcode (5.1.1 只能是此版本)

准备环境


1.创建大小写区分的磁盘分区

2.挂载刚刚创建好的分区

3. 切换到刚刚挂载的分区

4.安装最新版本的 repo

5. 安装依赖的第三方应用

6.下载并安装 Xcode 5.1.1此版本,我们只是使用其中的 SDK,其他的,我们使用8.2.1中的工具,注意,我们只能使用MacOSX10.8.sdk才能顺利编译通过。

下载地址 http://adcdownload.apple.com/Developer_Tools/xcode_5.1.1/xcode_5.1.1.dmg

下载之后,安装之前我们先把8.2.1的Xcode重命名一下,稍后我们再修改回来即可。我们安装之后,提取 Xcode 5.1.1版本里面的"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.8.sdk",然后拷贝到Xcode 8.2.1 版本的"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs"目录下面。操作完成后,可以删除 Xcode 5.1.1

也可以从phracker/MacOSX-SDKs 获取已经提取好了的版本。

执行如下命令,使得低版本的SDK生效。

如此操作的原因有两条:

(1)在macOS Sierra  (10.12.3)上, Xcode 5.1.1无法执行,因此没办法做到两个版本并存.

(2)如果通过修改代码来把代码中限制的系统版本调整到10.8版本以上,则GCC存在BUG,导致编译无法正常通过,具体的BUG参考
libgcc: error: unknown value '10.10' of -mmacosx-version-min

7.下载 Android-5.1.1_r38源代码(此处请准备梯子,或者自己从国内源进行替换)

编译代码


1.设置编译需要的环境信息

2.切换Java版本到1.7

3.清理代码,为编译准备干净的环境

4.引入编译环境变量

5.设置编译目标,此处我们指定编译 ARM下的完整调试版本

6.如果需要跟踪调试代码,建议编译为调试类型

7.编译

注意此处如果发生编译失败,原因基本上是编译顺序导致的引用出错,也就是某些模块还没有编译完成,其他模块已经开始尝试链接,导致依赖错误,此时只要把多线程并发编译修改成单线程编译即可,即直接执行

运行镜像


上面运行起来的镜像是从 /Volumes/android/Android_Source/out/debug/target/product/generic/hardware-qemu.ini即可读取配置信息的,但是这个文件直接修改无效,我们如果需要修改参数,只能从启动参数中设置。
比如我们如果需要增大内存,开启 GPU的支持,则执行如下命令:

参考链接


Android查看SELinux状态及关闭SELinux

SELinux2.6版本的 Linux内核中提供的强制访问控制 ( MAC)系统。对于目前可用的 Linux安全模块来说, SELinux是功能最全面,而且测试最充分的,它是在 20年的 MAC研究基础上建立的。 SELinux在类型强制服务器中合并了多级安全性或一种可选的多类策略,并采用了基于角色的访问控制概念。

selinux默认配置在 /etc/sysconfig/selinux

默认有三种级别

enforcing级别: Linuxselinux所设置的安全策略都会被启用.所有与 selinux安全策略有关的服务或者程序都会被策略阻止.也就是,所有操作都会进行权限检查。

permissive级别: Linuxselinux所设置的安全策略都会被启动,但是所有与 selinux安全策略有关的服务或者程序不会被策略阻止,但是会收到警告.也就是,所有操作都被允许(即没有 MAC),但是如果有违反权限的话,会记录日志

disabled级别:关闭 selinux,相当于系统没有安装 selinux一样.

一般可以通过 getenforce查看 selinux的运行级别.也可以通过 setenforce 0或者 1设置 selinux的运行级别,级别 表示 Permissive模式,级别 1表示 Enforcing模式. 至于 disabled模式和其他模式的切换只能修改配置文件,命令不起作用.其次,修改完成之后,必须重启系统才能够生效.

参考链接


Ubuntu 16.04下载Android源代码

由于众所周知的原因,我们是没办法正常下载 Android的源代码的,因此只能使用国内的镜像来操作了。

1.安装 repo工具

2.在需要存储代码的地方创建文件夹

3.使用镜像下载 Android源代码
omapzoom.org的镜像

清华大学的镜像

上面执行之后是拉取全部的代码。

如果要使用某个特定分支的版本的源代码的话,则则初始化的时候指定分支,比如我想要 Android 7.0.0_r21的分支,则执行如下命令

4.同步代码

5.列出全部分支

6.切换到指定分支

7.查看当前的分支

8.删除不用的本地分支

参考链接


Android Studio 2.2 启用代码混淆

在新版本的 Android Studio中开启混淆的方法如下:

具体解释一下 minifyEnabled用来影响是不是开启混淆, shrinkResources只有在 minifyEnabledtrue的情况下,才能有效,用来去除无效的资源文件。 proguard-android-optimize.txtAndroid SDK-> tools\proguard目录下, Google已经写好默认的混淆模板文件,其中的 proguard-android.txt默认没有配置代码优化,而 proguard-android-optimize.txt默认配置了代码优化,至于我们自己工程下面的 proguard-rules.pro文件,只要配置我们自定义的额外配置即可,其他的用默认配置即可。

顺便讲一下代码混淆的好处:

1.代码安全,不易理解,增加破解难度。

2.减小APK的体积,减少内存开销。

3.缩减类名,方法名的长度,减少CPU开销。

参考链接


Android Studio 混淆代码时提示 “Warning:org.apache.commons.httpclient.util.URIUtil: can't find referenced class org.apache.commons.codec.net.URLCodec”

Android混淆代码时提示" Warning:org.apache.commons.httpclient.util.URIUtil: can't find referenced class org.apache.commons.codec.net.URLCodec"
解决方法是 proguard-rules.pro中增加如下语句:

其他类似的警告信息,可以参考以上的方法,逐行添加即可。

参考链接


ProGuard error can't find superclass or interface org.apache.http.entity

QFix 探索之路 —— 手Q热补丁轻量级方案

导语

QFix 是手Q团队近期推出的一种新的 Android 热补丁方案,在不影响 App 运行时性能(无需插桩去 preverify)的前提下有效地规避了 dalvik 下”unexpected DEX”的异常,而且还是很轻量级的实现:只需调用一个很简单的方法就能办到。

热补丁方案及手Q上的使用

自2015年 Android 热补丁技术开始出现,之后各种方案和框架层出不穷,原创性的技术方案主要有以下几种:

101
手Q从去年开始研究补丁方案,当时微信的 Tinker 还没有推出,考虑到兼容性和稳定性,就选用了 Java 反射 hack classloader 的方案,而且和当时已经很成熟的分 dex 从原理上很类似,主要的难点是如何解决 Qzone 发现的 dalvik 下”unexpected DEX”异常,由于没有研究出其它方法,就沿用了 Qzone 原创的插桩去 preverify 的解决方案,自2016年1月热补丁开始在手Q正式版本投入使用,至今解决问题十多个,修复效果十分明显,稳定性也很好。

性能无法提升,需要改变

插桩的解决方案会影响到运行时性能的原因在于:app 内的所有类都预埋引用一个独立 dex 的空类,导致安装 dexopt 阶段的 preverify 失败,运行时将再次 verify+optimize。近期我们通过 ReDex 尝试优化手Q的启动性能时发现:

  • 保留手Q现有的插桩,启动性能没有任何优化效果;
  • 去掉插桩,优化手Q启动相关类的 dex 分布,启动性能提升 30%。

另外即使后期手Q的发布版本实际上无需发布补丁,我们也需要预埋插桩的逻辑,这本身也是不合理的一点,所以确实有必要去探索新的方向,既保留补丁的能力,同时去掉插桩带来的负面影响。

重新分析”unexpected DEX”异常

寻找新的解决方案,还是需要回过头来分析下这个异常出现的条件:

102
这是 dalvik 的一段源码,当补丁安装后,首次使用到补丁里的类时会调用到这里,需要同时满足图中标出来的三个条件,才能出现异常,这三个条件的含义如下:

103

可以看出,Qzone 的插桩方案是突破了条件2的限制(统一去掉了所有引用类的 preverify 标志),而微信 Tinker 的 dex 增量合成方案是突破了条件3的限制(将补丁和 app dex 合成后替换,原先 app 里在同一个 dex 的两个类,其中一个后来打在补丁里,合成后还是会在同一个 dex里),那有没有办法从条件1入手呢?条件1中 fromUnverifiedConstant 为 true 就行,其实之前就有从这个条件进行突破的方案:

http://blog.csdn.net/xwl198937/article/details/49801975

主要思路是:每当系统调用到这个方法,通过 native hook 拦截这个系统方法,更改这个方法的入口参数,将 fromUnverifiedConstant 统一改为 true,但和 Andfix 类似,native hook 方式存在各种兼容性和稳定性问题,而且拦截的是一个涉及 dalvik 基础功能同时调用很频繁的方法,无疑风险会大很多。

找到新的“大陆”

这段逻辑所在的方法是 dvmResolveClass,通过类之间的引用会调用这个方法,入口参数分别是引用类的 ClassObject,被引用类的 classIdx,以及引用关联的 dalvik 指令是否为 const-class/instance-of,返回的是被引用类的 ClassObject,经反复阅读分析,终于发现了一个可以利用的细节:

104
 

105

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

106
 

107
 

108

结合以上分析,我想到一个思路:只需首次引用到补丁类时能够成功突破上述三个条件之一的限制即可,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 上验证:

110

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

111

没那么简单,初步方案行不通

上面的 demo 预埋了补丁里包含的类,但在实际运用中我们是无法预先设定哪些类要打补丁的,dex 里对补丁类 const-class/instance-of 方式的引用指令是编译时确定的,但具体是哪些类又需要在运行时动态确定,所以这种动态方式行不通,最初想到的是类似插桩的做法,预先把 app 里所有类都以 const-class 方式引用一遍,但很明显有以下问题:

1)由于 App 里类的数量很多,所有类的预先引用统一放在一个地方肯定不现实,需要分散在多个区,只对补丁类所在的少数几个区执行预先引用的操作,但这里如何划分的粒度不好把握,而且 App 里的类及数量一直变化,我们做过一些尝试,但没有比较理想的可考量的方案。

2)预先引用解析所有类,会增加引用类的加载耗时和引用语句本身的执行耗时,对于执行耗时,可以通过添加条件判断来优化,如果要解析的类在补丁类名列表里就执行该语句,否则就不执行,对于加载耗时,初步的测试结果如下(这里一个划分的区包含500个左右的类,并进一步区分了是否 preverify,而测试的补丁包里包含2个类):

112

从测试数据看,加载的耗时较长,而且补丁类不可预期,如果不巧分布在多个区里,累计耗时的影响将会严重得多。

3)该方案实现起来特别繁琐,不实用。

确定最终方案

新的方案在 Java 层找不到可行的实现方式,就尝试从 native 层切入,只需首次引用解析补丁类时,直接通过 jni 调用 dalvik 的 dvmResolveClass 这个方法,当然传入的参数 fromUnverifiedConstant 需要设为 true,这个思路与前面说的 native hook 方式不同,不会去 hook 这个系统方法,而是从 native 层直接调用:

  1. dvmResolveClass 方法是在 dalvik 的系统库 /system/lib/libdvm.so 里,通过 dlopen 即可获取该系统库的句柄
  2. 通过 dlsym 获取 dvmResolveClass 这个方法的地址
  3. 设定 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:

113

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

114

这个过程可以通过一个小程序自动进行:

输入: 原有 apk 的所有 dex、补丁包所有的类名
输出: 补丁包每个类所在 dex 的编号以及 classIdx 的值
注1: 如果在补丁新增原 app 不存在的类,运行时新增类只会被补丁 dex 即同一个 dex 里的类所引用,所以新增的补丁类无需预先解析引用。
注2: 由于”unexpected DEX”异常出现在 dalvik 的实现里,art 模式下不会存在,以上预先引用补丁类的逻辑只需用在5.0以下的系统。

最终新方案的整体实现流程如下图所示:

115

可以看出,新的方案是很轻量级的实现,只需一个很简单的 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,经分析定位到原因如下:

116

可以看到dlopen libdvm.so时将库的名字改为了libvmkid_lemur.so,yunos的dalvik实现实际上在后面这个库里,而且通过反汇编发现导出的符号名也变化了,但内部的实现逻辑没有变化:

在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文件如下:

注意, lib_entry()必须以 exit(0)结束,否则会导致进程退出失败。

使用如下命令编译源代码:

-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的事务模型,由于事务一定要保证数据已经同步到磁盘了,因此,可以避免出现断电后数据由于没有刷新到磁盘导致的数据丢失。

很多时候,会发现直接通过 Kill,杀掉进程,一般是不会丢失数据的,原因在于磁盘写入的时候,系统会进行缓存,等合并到一定的量或者时间,系统一次性同步到磁盘,这样可以大大提供系统的性能。因此进程虽然已经死掉了,但是系统还是会把已经提交到内核的数据刷新到磁盘的,因此表现就是数据不会丢失。但是如果是断电的话,系统也就无能为力了。于是表现就是,越是新的 Linux内核版本,反倒越是在异常断电的时候容易丢失数据。

参考链接


SQLiteOpenHelper