Android部分技术点目录(3)
目录
Android插件化原理
oom_adj的具体计算方法
ActivityStack/ActivityRecord/TaskRecord关系
Binder深入
把XML文件inflate到界面上的全过程
try catch finally的几个坑
关于Thread
Android中有哪些解析xml的类
SIM卡的EF文件是什么?有何作用
段式和页式存储的区别
P、V原语
Android虚拟机、apk、aapt、dex、odex、deodex、smali
JVM、Dalvik和ART的区别
JVM基于栈,Dalvik基于寄存器
ART的GC优化
Android插件化原理
插件化就是动态加载apk,两个核心问题是加载代码和加载资源,但真正棘手的问题在于组件的生命周期和资源id冲突。
1.加载代码
需要一个ClassLoader来加载类。
1.1 ClassLoader
DexClassLoader和双亲委派。
利用当前的ClassLoader,新建DexClassLoader:
ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,
dexOutputPath, null, localClassLoader);
新的DexClassLoader以现有的ClassLoader为双亲,根据双亲委派机制,两个apk的代码就都能访问了。
2.加载资源
需要通过ContextImpl来获取资源,具体是通过getAssets和getResources来获取资源的。
2.1 AssetManager
Asset需要add新apk的AssetPath:
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(instance, dexPath);
2.2Resources
Resource需要使用增加过的Asset:
private Resources getResources(Context ctx, AssetManager selfAsset) {
DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
Configuration con = ctx.getResources().getConfiguration();
return new Resources(selfAsset, metrics, con);
}
3.生命周期管理
通过DexClassLoader通过反射创建出来的Activity是一个普通的java对象,并没有生命周期,这个问题有多个解决思路:
3.1 代理
在主应用的manifest中注册一个代理activity,并将proxy的生命周期,触摸按键回调等与插件Activity同步,实现插件Activity的生命周期管理。
3.2 替换ClassLoader
App的ClassLoader可以这样找到:ActivityThread-->LoadedApk-->mClassLoader。
然后,用反射,setFiledObject替换android.app.LoadedApk中的mClassLoader实例,这个实例需要mPackages,mPackages又需要currentActivityThread:
Object currentActivityThread = RefInvoke.invokeStaticMethod(
"android.app.ActivityThread", "currentActivityThread",
new Class[] {}, new Object[] {});//获取主线程对象
String packageName = this.getPackageName();//当前apk的包名
ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mPackages");
WeakReference wr = (WeakReference) mPackages.get(packageName);
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
wr.get(), dLoader);
这种方法下,声明组件,还是要在宿主App中声明。
而且,没有apk的资源包,不能再用setContentView,要额外做一个设置视图的功能。
3.3 插入dex
DexClassLoader和PathClassLoader(只能加载已安装的app)的父类BaseDexClassLoader,有一个Element[] dexElements。
将新的DexClassLoader中的Elements合并到PathClassLoader的数组中,也可以实现。
4.资源冲突
在apk编译过程中,资源文件会由aapt工具生成对应的ID,并在打包的时候组织成resources.arsc文件,resources.arsc文件是用来描述资源ID和资源位置配置信息,从18个维度描述了一个资源ID的配置信息(语言、分辨率等),就是资源ID和资源的索引表。资源的ID生成是有规则的,规则:0xPPTTNNNN,由8位16进制组成,其中:
PP段:表示资源的包空间:0x01表示系统资源空间,0x7f表示应用资源空间。
TT段:表示资源类型。
NNNN段:4个16进制表示资源id,一个apk中同一类型资源从0000开始递增。
因为宿主apk和插件apk是独立编译出来的两个独立的apk,那么其中就有资源ID相同的情况出现,从而产生资源ID冲突。Sophix的解决方法是把补丁中的resource用0x66替换0x7f。
Android中插件开发篇之----动态加载Activity(免安装运行程序)
Android动态加载技术三个关键问题详解
Android插件化系列第(二)篇---动态加载技术之apk换肤
oom_adj的具体计算方法
Linux有个OOM Killer,会监控那些占用内存过大,而且会瞬时消耗大量内存的进程,为防止内存耗尽,会强制杀死进程。
Linux内核会通过特定的算式计算每个进程的oom_score的值,oom_score为2的n次方,n为oom_adj的值,范围是-16~15,-17表示禁用OOM。
ActivityStack/ActivityRecord/TaskRecord关系
图片来自从源码角度看Activity launchMode与Stack/Task从源码角度看Activity launchMode与Stack/Task
Binder深入
在App端,Binder操作时通过Activity-->ContextImpl-->LoadedApk$ServiceDispatcher来操作的,最终操作者是最后面这个LoadedApk的子类ServiceDispacher,最关键的代码为:
public void doConnected(ComponentName name, IBinder service, boolean dead) {
...
// If there was an old service, it is now disconnected.
if (old != null) {
mConnection.onServiceDisconnected(name);//断开连接
}
if (dead) {
mConnection.onBindingDied(name);//死亡通知(binder服务意外死亡)
}
// If there is a new service, it is now connected.
if (service != null) {
mConnection.onServiceConnected(name, service);//连接
}
}
连接/连接断开通知
对于Binder来说,Binder的Service是一个存在于某个进程里的对象,系统用红黑树管理这些对象的引用,并提供给client。所以,如果进程尚未启动,没有注册这个Binder服务,系统就找不到该服务,无法提供给client。
因为建立服务的时长是不确定,所以需要在ServiceConnection中用回调来处理连接结果:
ServiceConnection connection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
死亡通知
服务进程关闭时,会释放进程资源,关闭文件等,此时调用Binder驱动的binder_release-->binder_Died,如果AIDL客户端注册过DeathRecipient接口,调用linkToDeath方法,就能收到死亡通知:
public class XX implements Binder.DeathReceipient{//注册接口
observer.asBinder.linkToDeath();//调用方法
@Override
public void binderDied(){}//收到回调
}
如果是BindService的用法,可以直接在ServiceConnection中,用IBinder去注册死亡通知:
ServiceConnection connection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//注册死亡通知
try {
service.linkToDeath(new IBinder.DeathRecipient() {
@Override
public void binderDied() {
}
}, IBinder.FLAG_ONEWAY);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
异常通知
DeadObjectException和RemoteException。
服务端进程死亡或缓冲内存区满,都会报DeadObjectException。
writeStrongBinder/readStrongBinder
为了实现双工通信,用来传送Binder对象的,例如startActivity时,会向AMS发送ApplicationThread Proxy对象:
data.writeStrongBinder();//data是Parcel对象
writeStrongBinder是调用native实现的
线程池
每个进程的binder线程池上限为15个,外加一个主binder线程。
如果线程都满,会在todo队列中阻塞等待
缓冲内存区
linux层对mmap的上限为4M,Binder申请的mmap更低,同步空间为1016K,异步空间为508K(一半)。
如果缓冲内存区满,会抛DeadObjectException。
传递Bitmap
如果通过广播传递Bitmap,是受1016K限制的,实际中,Bundler超过800K就会报TransactionTooLargeException。
但是如果通过AIDL传递Bitmap,只要超过128K,就会使用ashmem来传图片,mmap只传递ashmem的fd。
把XML文件inflate到界面上的全过程
整个过程分三部分:
1.连接系统服务,利用WMS获取Window的位置、层级、生命周期等;利用SurfaceFlinger获取Surface以便绘制界面;界面内容的定义是在Activity中实现的;
2.创建Activity界面,在attach时创建PhoneWindow,在setContent时创建DecorView,调用动画、生成状态栏、活动栏、以及内容控件mContentParent。(所以,动画、状态栏等属性,需要在setContent前设置);
3.填充DecorView,主要是把XML的layout文件,inflate到mContentParent中,把xml装进一个XmlResourceParser对象中,然后while解析xml结构,找到根节点(所以layout只能有一个最外层控件),用createViewFromTag创建根节点,用rInflateChildren遍历剩余的xml,最终生成xml对应的ViewTree,填充到DecorView的R.id.content里面。
try catch finally的几个坑
try catch finally中,四种情况:
1.try里system.exit时不走finally
2.在前面return或throw时,会先走finally,
3.但是finally不能改return的值,因为return的机制是个传值,把变量值赋给不知名变量,finally不会改变这个不知名变量
4.在finally里不能return,会导致数据不一致,会导致catch里throw不被执行
关于Thread
ActivityThread作为主线程,启动时最大的不同就是prepare(false),这个false表示线程不可以取消:
//ActivityThread源码
public static void prepareMainLooper() {
prepare(false);//quitAllowed参数为false,不允许主线程退出
Thread是线程类,并不是线程,线程需要交给虚拟机去调用本地方法(VMThread.create),从CPU上切割线程资源。
Thread既继承了Runnable,又可以构造传参Runnable,所以它最终都是启动一个线程,去运行用户指定的Runnable,所以Thread相当于一个中介。
线程的wait、sleep、join和interrupt
wait不是java.lang.Thread的方法,是java.lang.Object的,wait会释放对象锁。
sleep是抱着对象等时间(CPU不释放)
join是插入到当前线程,让自己先执行
yield是自己先暂停一下,看看是否有其他优先级更高的线程需要执行,此时锁和CPU都没有释放,如果直接回来,不用切换上下文
interrupt是中断前面的执行过程,比如被block在wait,sleep或join上时,interrupt可以立即唤醒并抛出InterruptedException
Thread.interrupt:中断
Thread.interrupted:返回是否中断,并清除中断状态
Thread.isInsterrupted:返回是否中断,不清除中断状态
Android中有哪些解析xml的类
SAX,流式处理,事件驱动,解析时为每种xml调用处理函数。
PULL,Android系统内置,也是流式处理,但解析返回的是数字,根据数字自己写解析方式,可以自己控制解析到哪里。
DOM,树状结构解析,可以直接访问文件的各个部分
SIM卡的EF文件是什么?有何作用
sim卡里有MF、DF、EF三种文件,前两种用于检索,只有EF可以存放数据。
MF,主文件Master File,只有文件头,sim卡唯一,存放sim卡的控制信息和管理信息。
DF,专用文件Dedicated File,只有文件头,相当于目录的根,存放整个目录的控制信息和管理信息。
EF,基本文件Elementary File,既有文件头,又有文件体,文件头存放文件位置和控制信息,文件体存放数据
段式和页式存储的区别
基础:
页和块,逻辑地址是连续的页,页对应在物理地址上是大小一致(4K)位置分散的块,用页表存放对应的页号和块号即可。
段和基址,每个段对应一个连续的物理内存,段的大小不固定,通过段号和段表,找到物理基址。
页式是一维的,块号是地址高位,块号&偏移量,即可拼出完整的物理地址。
段式是二维的,根据段号找到基址(一个维度),加偏移量(另一个维度)即可找到对应的物理地址。
段页式:先按逻辑分段,每个段内部又分页,地址结构是:段号+段内页号+页内地址
段式/页式需访问两次内存,段页式需访问三次内存。
P、V原语
P、V原语解决资源争用的问题,主要依靠S信号量,S代表可用的资源数,P代表取用资源,V代表返还资源
P原语时,S=S-1,表示准备取走一份资源,此时如果S>=0,代表不会取成负的,那么就可以正常取,如果S<0,就意味着没有资源了,那么就需要等待资源释放
V原语时,S=S+1,表示准备还回一份资源,此时如果S>0,代表还之前不是负的,那么没有别人在等;如果S<=0,代表之前是负的,有别人在等资源,那么就需要通知一个等待资源的人。
Android虚拟机、apk、aapt、dex、odex、deodex、smali
虚拟机,Android的虚拟机先有Dalvik,后有Art,这两者都为移动平台做了特殊优化
apk,apk其实是个rar,里面有这样几个主要对象:
1.META-INF:文件夹,应用签名
2.res:文件夹,资源文件
3.classes.dex:执行程序
4.resources.arsc:二进制资源文件
aapt,Android的apk安装包是由aapt(Android Asset Packaging Tool)打包的
dex,Dalvik VM Executors,其中最大的改变就是dex文件取代了jar文件,安卓系统第一次执行dex时,会用dexopt工具做优化,使更适合在当前设备上运行。
odex,Optimized Dalvik VM Executors,优化的dex文件,这是安卓系统做的,把dex优化为odex,并删除旧的dex,优化的odex在data/dalvik-cache目录下。
odex的优点:
1.避免第一次启动时加载速度慢(已经优化过)
2.减少RAM占用(不需要保存优化前的dex)
3.防反编译(系统里只有本地优化过的odex)
odex的缺点:
1.升级时容易失败导致强制关闭
2.占rom
3.rom不方便修改
deodex,第三方rom里基本都是deodex,apk里直接是dex,启动时从apk里读dex,并优化为odex
deodex的优点:
1.减少rom体积(不需要在data/dalvik-cache里寸odex)
2.方便rom的定制和移植
3.方便apk反编译和修改
缺点:
1.启动耗时,每次启动需要读出dex,优化为odex
smali,odex需要用baksmali转换为dex,再把dex转换成jar包,直接从odex转jar包的话,大部分会有编译错误,只能简单的阅读分析。
JVM、Dalvik和ART的区别
Dalvik和ART并不是Java虚拟机,因为他们并没有完全遵循Java虚拟机规范。
JVM运行class字节码文件,Dalvik运行dex文件(JIT模式),ART在安装时转换为oat机器码。
最主要的差别是,JVM是基于栈,而Dalvik是基于寄存器,指令集不同。
odex和oat都是保存在data/dalvik-cache目录下,不过还都是同名文件(dex后缀名)。
odex是dey字节码,oat是机器码。
JVM基于栈,Dalvik基于寄存器
首先,无论哪种虚拟机,都需要以下功能:
取指令
译码
执行
存结果
JVM是基于栈的,使用指令来载入和执行栈上的操作,需要大量的指令。
基于栈的时候,真正的运算都在操作数栈上运行,不在非栈的内存中执行,这样虚拟机可以无视物理结构,但是处理速度慢。
Dalvik改为基于寄存器,使用空间小。
基于寄存器时,没有操作数栈的概念,而是有很多虚拟寄存器,执行引擎从寄存器中找到操作数,取出操作数,进行运算(直接传入CPU运算,不需要先进操作数栈)。
基于寄存器与基于栈的虚拟机
ART的GC优化
Dalvik:Dalvik虚拟机的内存管理并不好,有三大问题:
1.GC时挂起所有进程,查找活动对象时,会挂起所有的线程,stop the world,引起中断和阻塞
2.内存碎片化严重(标记清除)
3.缺少大块的连续空间
ART:
1.划分了四个堆空间(参考G1分堆垃圾回收器),各自的回收策略不同:
永不回收:ImageSpace
Sticky Mark Sweep:AllocationSpace
Partial Mark Sweep:AllocationSpace、Large Object Space
(力度最大)Mark Sweep:AllocationSpace、Large Object Space、ZygoteSpace
其中,Large Object Space是不连续的空间。
2.可以并行处理GC(过程参考CMS并发标记扫描垃圾回收器):
GC过程分成多个子任务,交给线程池处理,只在标记脏数据时stop the world。
并行GC基本过程:
--获取堆锁
--并行标记
--释放堆锁
--挂起ART,标记脏数据(并行标记期间被修改的)
--恢复ART
--获取堆锁
--回收内存
--释放堆锁
ART不一定会并行GC,如果是分配内存时发现内存不足,执行GC_FOR_ALLOC,不能并行,最耗时;如果分配堆后,剩余空间低于阈值,并行GC。
3.定期压缩活动对象,避免内存碎片化(标记整理)
ART运行时垃圾收集(GC)过程分析
【原创】【Android】揭秘 ART 细节 ---- Garbage collection