从我写下 Android插件化原理解析 系列第一篇文章至今,已经过去了两年时间。这期间,插件化技术也得到了长足的发展;与此同时,React Native,PWA,App Bundle,以及最近的Flutter也如火如荼。由于实现插件化需要太多的黑科技,它给项目的维护成本和稳定性增加了诸多不确定性;我个人认为,2017年手淘Atlas插件化项目的开源标志着插件化的落幕,2018年Android 9.0上私有API的限制几乎称得上是盖棺定论了——曾经波澜壮阔的插件化进程必将要退出历史主流。如今的插件化技术朝两个方向发展:其一,插件化的工程特性:模块化/解耦被抽离,逐渐演进为稳定、务实的的组件化方案;其二,插件化的黑科技特性被进一步发掘,inline hook/method hook大行其道,走向双开,虚拟环境等等。

虽然插件化终将落幕,但是它背后的技术原理包罗万象,值得每一个希望深入Android的小伙伴们学习。

很遗憾曾经的系列文章没有写完,现在已经没机会甚至可以说不可能去把它完结了;不过幸运的是,我的良师益友包老师(我习惯称呼他为包哥)写了一本关于插件化的书——《Android插件化开发指南》,书中讲述了过去数年浩浩荡荡的插件化历程以及插件技术的方方面面;有兴趣的小伙伴可以买一本看看。

阅读全文 »

众所周知,Android P 引入了针对非 SDK 接口(俗称为隐藏API)的使用限制。这是继 Android N上针对 NDK 中私有库的链接限制之后的又一次重大调整。从今以后,不论是native层的NDK还是 Java层的SDK,我们只能使用Google提供的、公开的标准接口。这对开发者以及用户乃至整个Android生态,当然是一件好事。但这也同时意味着Android上的各种黑科技有可能会逐渐走向消亡。

作为一个有追求的开发者,我们既要尊重并遵守规则,也要有能力在必要的时候突破规则的束缚,带着镣铐跳舞。恰好最近有人反馈 VirtualXposed 在 Android P上无法运行,那么今天就来探讨一下,如何突破Android P上针对非SDK接口调用的限制。

阅读全文 »

Xposed是Android系统上久负盛名的一个框架,它给了普通用户任意 DIY 系统的能力;比如典型的微信防撤回、自动抢红包、修改主题字体,以及模拟位置等等等等。不过,使用Xposed的前提条件之一就是需要Root。随着Android系统的演进,这一条件达成越来越难了;那么,能不能不用Root就可以享用Xposed的功能呢?

我们想一下,Xposed为什么需要Root?从现在的实现来看,因为Xposed需要修改系统文件,而这些文件只有root权限才能修改;但是这只是当前实现的特性(修改系统分区文件),而非根本原因。Xposed要实现的最终目的是在任意App进程启动之前能任意加载 特定Xposed模块 的代码;这些特定的Xposed模块中能在App进程启动之前有机会执行特定代码,从而控制任意进程的行为。归根结底,Xposed需要控制别的进程,而没有高级权限(Root),越俎代庖是不行的。

有没有别的实现方式?

阅读全文 »

两年前阿里开源了Dexposed 项目,它能够在Dalvik上无侵入地实现运行时方法拦截,正如其介绍「enable ‘god’ mode for single android application」所言,能在非root情况下掌控自己进程空间内的任意Java方法调用,给我们带来了很大的想象空间。比如能实现运行时AOP,在线热修复,做性能分析工具(拦截线程、IO等资源的创建和销毁)等等。然而,随着ART取代Dalvik成为Android的运行时,一切都似乎戛然而止。

今天,我在ART上重新实现了Dexposed,在它能支持的平台(Android 5.0 ~ 7.1 Thumb2/ARM64)上,有着与Dexposed完全相同的能力和API;项目地址在这里 epic,感兴趣的可以先试用下:) 然后我们聊一聊ART上运行时Method AOP的故事。

阅读全文 »

Android上的热修复框架 AndFix 想必已经是耳熟能详,它的原理实际上很简单:方法替换——Java层的每一个方法在虚拟机实现里面都对应着一个ArtMethod的结构体,只要把原方法的结构体内容替换成新的结构体的内容,在调用原方法的时候,真正执行的指令会是新方法的指令;这样就能实现热修复,详细代码见 AndFix

为什么可以这么做呢?那得从 Android 虚拟机的方法调用过程说起。作为一个系列的开篇,本文不打算展开讲虚拟机原理等内容,首先给大家一道开胃菜;后续我们再深入探索ART。

众所周知,AndFix是一种 native 的hotfix方案,它的替换过程是用 c 在 native层完成的,但其实,我们也可以用纯Java实现它!而且,代码还非常精简,且看——

阅读全文 »

半年前写了一篇文章,介绍 如何调试Android Framework,但是只提到了Framework中Java代码的调试办法,但实际上有很多代码都是用C++实现的;无奈当时并并没有趁手的native调试工具,无法做到像Java调试那样简单直观(gdb+eclipse/ida之流虽然可以但是不完美),于是就搁置下了。

Android Studio 2.2版本带来了全新的对Android Native代码的开发以及调试支持,另外LLDB的Android调试插件也日渐成熟,我终于可以把这篇文章继续下去了!本文将带来Android Framework中native代码的调试方法。

在正式介绍如何调试之前,必须先说明一些基本的概念。调试器在调试一个可执行文件的时候,必须知道一些调试信息才能进行调试,这个调试信息可多可少(也可以没有)。最直观的比如行号信息,如果调试器知道行号信息,那么在进行调试的时候就能知道当前执行到了源代码的哪一行,如果调试器还知道对应代码的源文件在哪,那么现代IDE的调试器一般就能顺着源码带你飞了,这就是所谓的源码调试。相反,如果没有行号和源码信息,那么只能进行更低级别的调试了,调试器只能告诉你一些寄存器的值;而当前运行的代码也只是PC寄存器所指向的二进制数据,这些数据要么是虚拟机指令,要么是汇编指令;这就是所谓的无源码调试。显然无源码调试相比源码级别的调试要麻烦的多;接下来将围绕这两个方面分别介绍。

阅读全文 »

有史以来,Android源码编译环境的搭建始终是一件麻烦事儿。网上有数不清的文章介绍如何编译Android源代码,但是他们要么方法复杂、步骤太多;要么自称解决了一些编译问题(需要修改头文件,系统配置等),让人对其可信度产生质疑。有的童鞋硬着头皮照做了,但是由于伟大的GFW,大部分都死在了第一步——repo脚本都下载不下来,就算下载过了过不了gerrit那一关。另外,就算你具备科学上网的能力,下载时间又成为了拦路虎;普通的VPN通常需要下载七八个小时,简直就是痛不欲生。久而久之,很多人对下载编译Android源码望而却步。

今天,我给大家提供一个极其简单、稳定的方案,来解决Android源码的下载编译问题。

首先,下载问题可以通过镜像解决;清华镜像科大镜像 都是非常不错的选择,正常情况下一到两个小时即可下载完一个Android源码分支。

然后就是编译环境问题。由于Android源码庞大,依赖复杂;一旦使用的编译工具链有细微的不同就可能引发编译失败。官方文档 推荐使用Ubuntu 14.04进行编译。如果我们用Windows或者Mac系统,传统方式是使用虚拟机;但是在今天,我们完全可以使用 Docker 替代!!借助Docker,我们可以不用担心编译环境问题;不论我们的开发机是什么系统,可以使用Docker创建Ubuntu Image,并且直接在这个Ubuntu系统环境中创建编译所需要的工具链(JDK,ubuntu系统的依赖库等等);而且,Docker运行的Ubuntu的系统开销比虚拟机低得多,这样下载以及编译速度就有了质的提升。更重要的是,这个环境可以作为一个Image打包发布!这样,你在不同的开发机,还有你与你的同事之间有了同一套编译环境,这会省去很多不必要的麻烦。关于Docker的更多内容,见 Docker官网

阅读全文 »

介绍完 深入学习Android:虚拟机&运行时 之后,很多小伙伴问我,你描述的这些知识结构看起来艰深晦涩高大上,实际工作中能有多大用途呢?今天我就简单举个例子。

众所周知,我们的Android App运行在Java虚拟机之上,而Java是一门带GC的语言。在虚拟机进行垃圾回收的时候,要做一件很形象的事叫做STW(stop the world);也就是说,为了回收那些不再使用的对象,虚拟机必须要停止所有的线程来进行必要的工作。虽说这一点在ART运行时上得到了很大的改善,但是GC的存在对App运行时的性能始终有着微妙的影响。如果你观察过手机输入的日志,一定会看到类似如下的内容:

12-23 18:46:07.300 28643-28658/? I/art: Background sticky concurrent mark sweep GC freed 15442(1400KB) AllocSpace objects, 8(128KB) LOS objects, 4% free, 32MB/33MB, paused 10.356ms total 53.023ms at GCDaemon thread CareAboutPauseTimes 1
12-23 18:46:12.250 28643-28658/? I/art: Background partial concurrent mark sweep GC freed 28723(1856KB) AllocSpace objects, 6(92KB) LOS objects, 11% free, 31MB/35MB, paused 2.380ms total 108.502ms at GCDaemon thread CareAboutPauseTimes 1

上面的日志反映一个事实:GC是有代价的。有很多有关性能优化的文章提到GC,会花长篇大论讲述垃圾回收的过程以及原理,但所做的策略无非就是「不要创建不必要的对象」,「避免内存泄漏」最终就提到MAT,LeakCanary等工具的使用上去了;我只能说这很苍白无力——写出这样的代码、学会使用工具应该是基本要求。

阅读全文 »

SharedPreference是Android上一种非常易用的轻量级存储方式,由于其API及其友好,得到了很多很多开发者的青睐。但是,SharedPreference并不是万能的,如果把它用在不合适的使用场景,那么将会带来灾难性的后果;本文将讲述一些SharedPreference的使用误区。

存储超大的value

第一次看到下面这个sp的时候,我的内心是崩溃的:

一个默认的sp有90K,当我打开它的时候,我都快哭了:除了零零星星的几个很小的key之外,存储了一个炒鸡大的key,这一个key至少占了其中的89K。知道这是什么概念吗?

在小米1S这种手机上,就算获取这个sp里面一个很小的key,会花费120+ms!!那个毫不相干的key拖慢了其他所有key的读取速度!当然,在性能稍好的手机上,这个问题不是特别严重。但是要知道,120ms这个是完全不能忍的!

之所以说SharedPreference(下文简称sp)是一种轻量级的存储方式,是它的设计所决定的:sp在创建的时候会把整个文件全部加载进内存,如果你的sp文件比较大,那么会带来两个严重问题:

  1. 第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。
  2. 解析sp的时候会产生大量的临时对象,导致频繁GC,引起界面卡顿。
  3. 这些key和value会永远存在于内存之中,占用大量内存。
阅读全文 »

目前为止我们已经完成了Android四大组件中Activity,Service以及BroadcastReceiver的插件化,这几个组件各不相同,我们根据它们的特点定制了不同的插件化方案;那么对于ContentProvider,它又有什么特点?应该如何实现它的插件化?

与Activity,BroadcastReceiver等频繁被使用的组件不同,我们接触和使用ContentProvider的机会要少得多;但是,ContentProvider这个组件对于Android系统有着特别重要的作用——作为一种极其方便的数据共享的手段,ContentProvider使得广大第三方App能够在壁垒森严的系统中自由呼吸。

在Android系统中,每一个应用程序都有自己的用户ID,而每一个应用程序所创建的文件的读写权限都是只赋予给自己所属的用户,这就限制了应用程序之间相互读写数据的操作。应用程序之间如果希望能够进行交互,只能采取跨进程通信的方式;Binder机制能够满足一般的IPC需求,但是如果应用程序之间需要共享大量数据,单纯使用Binder是很难办到的——我相信大家对于Binder 1M缓冲区以及TransactionTooLargeException一定不陌生;ContentProvider使用了匿名共享内存(Ashmem)机制完成数据共享,因此它可以很方便地完成大量数据的传输。Android系统的短信,联系人,相册,媒体库等等一系列的基础功能都依赖与ContentProvider,它的重要性可见一斑。

既然ContentProvider的核心特性是数据共享,那么要实现它的插件化,必须能让插件能够把它的ContentProvider共享给系统——如果不能「provide content」那还叫什么ContentProvider?

但是,如果回想一下Activity等组件的插件化方式,在涉及到「共享」这个问题上,一直没有较好的解决方案:

阅读全文 »