Android知识梳理

2023-05-16

Android中5种布局 :FrameLayout,LinearLayout, RelativeLayout, TableLayout全部继承ViewGroup

Activity和Fragment的关系: onAttach(fragment)--> onCreate(fragment)-->onCreateView(fragment)-->onViewCreated(fragment)-->onCreate(activity)-->onStart(Activity)-->onStart(fragment)-->onResume(activity)-->onResume(fragment)-->onPouse(fragment)-->onPouse(Activity)->onStop(fragment)->onStop(activity)-->onDestoryView(fragment)-->onDestory(fragment)-->onDetach(fragment)-->onDestory(Activity)

为什么在Service中创建子线程而不是Activity中

因为在Activity中创建Thread很难对其进行控制,当Activity被销毁之后,就没有任何其他方法可以重新获取到之前的子线程的实例。而且在一个Activity中创建的子线程另一个Activity无法对其进行操作。但是Service就不同了,所有的Activity都可以与Service进行关联,然后可以很方便地操作其中的方法,即使Activity被销毁了,之后只要重新与Service建立关联,就又能够获取到原有的Service中Binder的实例。

能否保证service不被杀死

Service设置成START_STICKY,kill后会被重启,重传Intent,保持与重启前一样。

提升service优先级

在AndroidManifest文件中对intent-filter可以通过android:priority="1000"这个属性设置最高优先级,1000是最高值。

目前来看,priority这个属性貌似只适用于broadcast,对于Service来说可能无效。

提升service进程优先级

Android中的进程是托管的,当系统进程空间紧张的时候,会依照优先级自动进行进程的回收

当service运行在低内存的环境时,将会kill掉一些存在的进程。因此进程的优先级将会很重要,可以在startForeGound()使用startForegound()将service放到前台状态。这样在低内存时被kill的几率会低一些。

如果在极度低内存的压力下,该service还是会被kill掉,并且不一定会restart

onDestroy方法里重启service

service+broadcast方式,就是当service走onDestory的时候,发送一个自定义的广播,当收到广播的时候,重启service

也可以直接在onDestory里startServie

监听系统广播判断Service状态

通过系统的一些广播,比如:手机重启,界面唤醒,应用状态改变等等监听并捕获到,然后判断我们的servie是否存活。

在jni层,用C代码fork一个进程出来

这样产生的进程,会被系统认为是两个不同的进程。

动画有哪两类,各有什么特点?三种动画的区别

tween补间。通过指定View的初末状态和变化时间、方式,对View的内容完成一系列的图形变换来实现动画效果。alpha scale translate rotate

frame 帧动画AnimationDrawable控制animation-listxml布局

propertyAnimation属性动画

如何判断应用被强杀

在Application中定义一个static常量,赋值为-1,在欢迎界面改为0,如果被强杀,application重新初始化,在父类Activity判断该常量的值。

应用被强杀如何解决

所有的类继承一个父类中,在父类中进行判断,如果被强杀,跳转到欢迎页面,重新来一遍流程。

Asset目录与res目录的区别。res目录下面有很多文件,例如drawable,mipmap,raw等,除了raw文件不会被压缩外,其他文件都会被压缩。同时res目录下的文件可以通过R文件访问。Asset也是用来存储资源,但是asset文件内容只能通过路径或者AssetManager读取。

如何自定义控件

1 自定义属性的声明和获取(. 分析需要的自定义属性 . 在res/values/attrs.xml定义声明 . 在layout中使用 . 在view的构造方法中进行获取)

2 测量onMeasure 3 布局onLayout 4 绘制onDraw 5 onTouchEvent 6 onInterceptTouchEvent 7状态的恢复和保存。

Android长连接,怎么处理心跳机制

1 心跳机制是定时发送一个自定义的结构体(心跳包),让对方知道自己还活着,以确保连接的有效性的机制。

2 android系统的推送和ios的推送有什么区别

首先我们必须知道,所有的推送功能必须有一个客户端和服务器的长连接,因为推送是由服务器主动向客户端发送消息,如果客户端和服务器之间不存在一个长连接,那么服务器是无法来主动连接客户端的。因而推送功能都是基于长连接的基础上的。

IOS长连接是由系统来维护的,也就是说苹果的IOS系统在系统级别维护了一个客户端和苹果服务器的长链接,IOS上的所有应用上的推送都是先将消息推送到苹果的服务器然后将苹果服务器通过这个系统级别的长连接推送到手机终端上,这样的的几个好处为:

在手机终端始终只要维护一个长连接即可,而且由于这个长连接是系统级别的不会出现被杀死而无法推送的情况。

省电,不会出现每个应用都各自维护一个自己的长连接。

安全,只有在苹果注册的开发者才能够进行推送,等等。

android的长连接是由每个应用各自维护的,但是google也推出了和苹果技术架构相似的推送框架,C2DM,云端推送功能,但是由于google的服务器不在中国境内,所以导致这个推送无法使用,android的开发者不得不自己去维护一个长连接,于是每个应用如果都24小时在线,那么都得各自维护一个长连接,这种电量和流量的消耗是可想而知的。虽然国内也出现了各种推送平台,但是都无法达到只维护一个长连接这种消耗的级别。

推送的实现方式

客户端不断的查询服务器,检索新内容,也就是所谓的pull或者轮询方式

客户端和服务器之间维持一个TCP/IP长连接,服务器向客户端push

服务器有新内容时,发送一条类似短信的信令给客户端,客户端收到后从服务器中下载新内容,也就是SMS的推送方式。

在TCP机制里面,本身是存在有心跳包机制的,也就是TCP选项SO_KEEPALIVE,系统默认是设置的2小时的心跳频率。

心跳包的机制,其实就是传统的长连接。或许有的人知道消息推送的机制,消息推送也是一种长连接 ,是将数据有服务器端推送到客户端这边从而改变传统的“拉”的请求方式。

下面介绍一下安卓服务端和客户端两个数据请求的方式

push 这个也就是由服务器推送到客户端这边 现在有第三方技术,比如极光推送。

pull 这种方式就是客户端向服务器发送请求数据(http请求)

Retrofit基本原理

Retrofit是一个Android网络框架。是一个对OKHttp的简单封装。所以内部实现原理其实就是基于OKHttp的请求方式。Retrofit采用注解方式开发。通过注解构建不同的请求和请求的参数,省去了创建大量类似的请求与方法,实际上这些参数最终都会在OKHttp中组合成一个完整的Http的请求(包含请求头和请求体),并通过OKHttp框架进行发送。

Glide 和 Picasso的区别

从功能实现上Glide和Picasso非常相似,功能基本重复。但是Picasso的默认Bitmap图片格式是ARGB-8888而Glide默认是RGB-565,Picasso的性能开销是大于Glide。

原因是Picasso是加载全尺寸的图片到内存,然后让Gpu来实时重绘大小。而Glide得大小是ImageView是一致的,因此更小。当然Picasso也可以指定加载的图片大小的。

Picasso.with(this).load("url").resize(768,432).into(ivImageView);

但是问题在于需要自己主动计算imageView的大小,或者说你的imageView大小是具体的数值(而不是wrap_content),你也可以这样

Picasso.with(this).load("url").fit().centerCrop().into(ivImageView);
这样处理后,PicassoGlide的内存开销差不多了。

虽然内存开销差不多了,但是从功能使用上还是Glide使用方便。因为Glide可以自动计算出任意情况下ImageView的大小。

Picasso和Glide在磁盘缓存策略上有很大的不同。Picasso缓存的是全尺寸的,而Glide缓存的是ImageView的大小。不论如何改变ImageView的大小,Picasso缓存都是图片的全尺寸的,Glide则需要将每个不同尺寸的ImageView都需要重新下载缓存。

不过可以改变这种行为,让Glide既缓存全尺寸又缓存其他尺寸:

Glide.with(this).load("url").diskCacheStrategy(DiskCacheStrategy.All).into(ivImageView);

下次在任何imageView中加载图片的时候,全尺寸的图片将从缓存中取出,重新调整大小,然后缓存。。

Glide的这种方式的优点加载速度显示非常快。而Picasso的方式因为需要在显示之前重新调整大小而导致一些延迟,即便你添加了这段代码来让其立即显示:

.noFade();

Glide可以加载gif图,而Picasso做不到,同时因为Glide和Activity,Fragment生命周期是一致的,因此gif的动画也会自动的随着Activity和Fragment的状态暂停,重放。Glide的缓存在gif这里也是一样,调整大小然后缓存。

Gilide可以将任何的本地视频解码成一张静态图片。

还有一个特性是你配置图片显示的动画,而Picasso只有一种动画:fading in。

Android5.0,6.0新特性

Android5.0:MaterialDesign设计风格,支持多种设备,支持64位ART虚拟机。

Android6.0:支持快速充电的切换,支持文件夹拖拽应用,相机新增专业模式。

Android7.0:分屏多任务,增强的java8语言模式,夜间模式。

Context区别

1 Activity和Service以及Application的Context是不一样的,Activity继承自ContextThemeWraper。其他的继承自ContextWrapper。

每一个Activity和Service以及Application的Context都是一个新的ContextImpl对象。

getApplication()用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在绝大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其他的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getAppContext()方法,getApplicationContext()比getApplication()方法作用域更广一些,任何一个Context的实例,只要调用getApplicationContext()方法都可以拿到我们的Application对象。

Activity在创建的时候会new一个Contextimpl对象并在attach方法中关联它,Application和Service也差不多。contextWrapper的方法内都是转调ContextImpl的方法。

创建对话框传入Application的Context是不可以的。

尽管Application、Activity、Service都有自己的Contextimpl,并且每个Contextimpl都有自己的mResources成员,但是由于它们的mResources成员都来自于唯一的ResourceManager实例,所以它们看似不同的mResource其实都是指向的是同一快内存。

Context的数量等于Activity的个数+Service的个+1,这个1为Application。

IntentServie是Service的子类,是一个异步的自动停止的服务,很好解决了传统的Service中处理完耗时操作忘记停止销毁Service的问题。

查看每个应用程序最高可用内存: int maxMemory = (int)(Runtime.getRuntime().maxMemory()/ 1024);

如何自学Android

首先看书(Android群英传)和一些大牛的博客,做一些项目,写一些csdn。

java内存分配策略

java程序运行时的内存分配策略有三种:1 静态分配,栈式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区),栈区和堆区。

静态存储区(方法区):主要存放静态数据、全局static数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

栈区:当方法执行的时候,方法体内的局部变量(其中包括基础数据、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所所持有的内存将会自动被释放。因为栈内存分配内置于处理器的指令集中,效率很高,但是内存容量有限。

堆区:又称动态分配内存,通常就是指在程序运行时直接new出来的内存,也就是对象的实例。这部分内存在不使用时将会由java垃圾回收器负责回收。

栈与堆得区别:

在方法体内定义(局部变量)的一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,java就会在栈中为该变量分配内存空间,当超过该变量的作用域时,该变量的也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用来存放所有由new创建的对象(包括对象中所有成员变量)和数组。在堆中分配的内存,将由java垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

局部变量的基本数据类型引用存储在栈中,引用的对象实体存储在堆中。因为他们是属于方法中的变量,生命周期随方法而结束。

成员变量全部存储在堆中(包括基本类型,引用和引用的对象实体)。因为他们属于类,类对象终究要被new 出来使用的。

GC为了正确释放对象,它必须监控每一个对象的运行状态,包括对象的申请,引用,被引用,赋值等,GC都需要进行监控。

监控对象状态是为了更加准确,及时的释放对象,释放对象的根本原则是该对象不再被引用。

java中的内存泄漏

在java中,内存泄漏就是存在一些被分配的对象,这些有两个特点,首先,这些对象是可达的,既在有向图中,存在通路可以与其相连;其次,这些对象是无用的,既程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为java中内存泄漏。这些对象不会被GC回收,然而它却占用内存。

对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据java语言规范定义,该函数不保证jvm的垃圾回收器一定会执行。因为,不同的jvm实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。jvm调用GC的策略也有很多种,有的是内存使用达到一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行,有的是中断执行GC。但通常来说,我们不需要关系这些。除非在一些特定的场合,GC的执行影响应用程序的性能。例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓释放内存,例如将垃圾回收分解为一系列的小步骤执行。

1 java垃圾回收机制

不论哪种语言的分配方式,都需要返回分配内存的真实地址,也就是返回一个指针到内存块的首地址。java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆中分配的,所有对的象回收都是通过垃圾回收机制完成的。GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可达,则就将其回收,这样也可以消除引用循环的问题。在java语言中,判断一个内存空间是否符合垃圾收集标准有两个:一个是对象赋予了空值null,以下再没有调用过,另一个是给对象赋予了新值,这样重新分配了内存空间。

2 java内存泄漏引起的原因

内存泄漏时指无用的对象(不再使用的对象)持续占有内存或者无用的对象的内存得不到释放,从而造成内存空间的浪费称为内存泄漏。

java内存泄漏的根本原因是什么?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是java中内存泄漏的发生场景。

1 静态集合类引起的内存泄漏:

像HashMap、Vector等的使用最容易出现内存泄漏,这些静态变量的生命周期和应用程序是一致的,他们所引用的所有的对象Object也不能被释放,因为他们也将一致被Vector等引用着。

2 当集合里面的对象属性被修改后,再调用remove()方法时不起作用。

3 监听器

在java中,我们需要和监听器打交道,通常一个应用当中会用到很多监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

4 各种连接

比如数据库连接,网络连接和io连接,除非其显示调用了close()方法将其连接关闭,否则是不会自动被GC回收的,对于Resultset和Statement对象可以不进行显示回收,但Connection一定要显示回收,因为Connection在任何情况下都无法自动回收,而Connection一旦回收,Resultset和Satement对象就会立即为null。但是如果使用连接池,情况就不一样了,除了要显示地关闭连接,还必须显示关闭Resultset 和Satement对象,否则就会造成大量的Satement对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。

5内部类和外部模块的引用

内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用。

6 单例模式

不正确的使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在jvm的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被GC正常回收,导致内存泄漏。

Android中常见的内存泄漏汇总

1 集合类泄漏

集合类如果仅仅添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性,全局性的map等既有静态引用或者final一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。

2 单例造成的内存泄漏

由于单例的静态特性使得其生命周期跟应用的一样长,所以使用不当的话,很容易造成内存泄漏。

3 匿名内部类/非静态内部类和异步线程

非静态内部类创建静态实例造成的内存泄漏,在Activity中创建了一个非静态内部类的静态实例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正常的做法:将该内部类设置为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Contex,尽量使用Application的Context,当然Application的Context不是万能的,所以也不能随便乱用,对于有些则必须使用Activity的Contxt。

4 匿名内部类

Android开发经常会继承实现Activity/Fragment/View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄漏。

5 Handler的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免ANR而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,但是Handler不是万能的,对于Handler的使用代码编写一不规范既有可能造成内存泄漏。另外,我们知道Handler、Message和MessageQueue都是相互关联在一起的,万一Handler发送的Message尚未被处理,则该Message及发送它的Handler对象就被线程MessageQueue一直持有。由于Handler属于TLS(Thread Local Storage)变量,生命周期和Activity是不一致的。因此这种实现方式一般很难保证跟View或者Activity的生命周期保持一致,故很容易导致无法正确释放。

修复方法:在Activity中避免使用非静态内部类,比如上面我们将Handler声明为静态的,则其存活跟Activity的生命周期就无关了。同时通过弱引用引入Activity,避免直接将Activity作为Context传进去。

在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而生命周期较长的时候,可以尽量使用软引用和弱引用技术。

软/弱引用可以和一个引用队列联合使用,如果软引用所引用的对象被垃圾回收器回收,jajva虚拟机就会把这个软引用加入到与之关联的引用队列中,利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

创建静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,但是这样做虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能有待处理的消息,所以我们在Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destory时或者Stop时应该移除消息队列MessaeQueue中的消息。

public final void removeCallbacks(Runnable r);
public fianl void removeCallbacks(Runnable r, Object token);
public final void removeCallbacksAndMessages(Object token);
public final void removeMessages(int what);
public fianl void removeMessages(int what);
public final void removeMessages(int what,Object object);

尽量避免使用static成员变量

如果成员变量被声明为static,那我们都知道其生命周期将与整个应用生命周期一样。

这就会导致一系列问题,如果你对app进程设计上是长驻内存的,那即使切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,如果此app做过进程互保,那会造成app在后台频繁重启。

这里修复的方法是:不要在类初始时初始化静态成员。可以考虑lazy初始化。架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期有责任管理起来。

资源未关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File,游标Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

一些不良代码造成的内存压力

有些代码并不造成内存泄漏,但是它们,或是堆没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。

比如:Bitmap没调用recycle方法,对于Bitmap对象在不使用的时候,我们应该先调用recycle释放内存,然后将它设置为null,因为加载Bitmap对象所分配的内存空间,一部分是java的,一部分是c的(因为Bitmap分配的底层是通过JNI调用的)。而这个recycle就是针对C部分的内存释放。

总结

对Activity等组件的引用应该控制在Activity的生命周期之内;如果不能就考虑使用getApplicationContext或者getApplication,以避免Activity被外部长生命周期的对象持有而引发内存泄漏。

尽量不要在静态变量或者静态内部类中使用非静态外部成员变量,即使使用,也要考虑适时把外部成员变量置为空;也可以在内部类中使用弱引用来引用外部类的变量。

对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:

将内部类改为静态内部类
静态内部类中使用弱引用来引用外部类的成员变量

Handler的持有的引用对象最好使用弱引用,资源释放时也可以清空Handler里面的消息,比如在Activity onStop或者onDestory的时候,取消掉该Handler对象的Message和runnable。

在java的实现过程中,也要考虑其对象的释放,最好的方法是在不使用某对象时,显示地将此对象赋值为null,比如使用完Bitmap后先调用recycle(),再赋值null,清空对图片资源有直接引用或者间接引用的数组(array.clear();array = null)等,最好遵循谁创建谁释放的原则。

正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。

保持对对象生命周期的敏感,特别注意单例,静态对象,全局性集合等的生命周期。

Handler内存泄漏分析及解决

public class SampleActivity extends Activity{
    private final Handler mLeakHandler = new Handler(){
        @Overide
        public void handleMessage(Message msg){
            
        }
    }
}

在使用Handler时,这是一段很常见的代码。但是,它却会造成严重的内存泄漏问题。在实际编写中,我们往往会得到如下警告:

In Android , Handler classes should be static or leaks might occur。

分析

1 Android角度

当Android应用程序启动时,framework会为该应用程序的主线程序创建一个Looper对象。这个Looper对象包含一个简单的消息队列Message Queue,并且能够循环的处理队列中的消息。这些消息包括大多数应用程序framework事件,例如Activity生命周期方法的调用、button点击等,这些消息都会被添加到消息队列中并被逐个处理。

另外,主线程的Looper对象会伴随该应用程序的整个生命周期。

然后,当主线程里,实例化一个Handler对象后,它就会自动与主线程Looper的消息队列关联起来。所有发送消息队列的消息Message都有拥有一个对Handler的引用,所以当Looper来处理消息时,会据此回调(handleMessage())方法来处理消息。

2 java角度

在java里,非静态内部类和匿名类都会潜在的引用它们所属的外部类。但是,静态内部类却不会。

public class SampleActivity extends Activity {

  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}
  • 当Activity结束finish时,里面的延时消息在得到处理前,会一直保存在主线程的消息队列里持续10分钟。而且,由上下文可知,这条消息持有对Handler的引用,而Handler又持有对其外部类(在这里,即SampleActivity)的潜在引用。这条引用关系会一直保持直到消息得到处理,从而,这阻止了SampleActivity被垃圾回收器回收,同时造成了内存泄漏。上面代码中的Runnable类--非静态匿名内部类--同样持有对其外部类的引用。从而导致泄漏。

上面已经明确了内存泄漏的来源:只要有未处理的消息,那么消息会引用Handler,非静态的Handler又会引用外部类,既Activity,导致Activity无法被回收,造成内存泄漏。

为了解决遇到的问题,我们要明确一点:静态内部类不会持有外部类的引用。所以,我们可以把Handler类放在单独的类文件中,或者使用静态类文件中,或者使用静态内部类便可以避免泄漏。

另外,如果想要在Handler内部去调用所在的外部类Activity,那么可以在Handler内部使用弱引用的方式指向所在Activity,这样不会导致内存泄漏。

对于匿名内部类Runnable,同样可以将其设置为静态类。

小结

虽然静态类和非静态类之间差别不大,但是对于Android开发者而言却是必须理解的。至少我们要清楚,如果一个内部类实例的生命周期比Activity更长,那么我们千万不要使用非静态的内部类。最好的做法是,使用静态内部类,然后在该类中使用弱引用来指向所在的Activity。

#Android性能优化 ##合理管理内存

节制的使用Service如果应用程序需要使用Service来执行后天任务的话,只有当任务正在执行的时候才应该让Service运行起来。当启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,系统可以LRUcache当中缓存的进程数量也会减少,导致切换程序的时候耗费更多性能。我们可以使用Intentservice,当后台任务执行结束后自动停止,避免Service的内存泄漏。

当界面不可见时释放内存,当用户打开了另一个程序,我们的程序界面已经不可见的时候,我们应当将所有和界面相关的资源进行释放。重写Activity的onTrimMemory()方法,然后在这个方法中监听TRIM_MEMORY_UI_HIDDEN这个级别,一旦触发说明用户离开了程序,此时就可以进资源释放操作了。

当内存紧张释放内存onTrimMemory()方法还有很多其他类型的回调,可以在手机内存降低的时候及时通知我们,我们应该根据回调中传入的级别来去决定如何释放应用程序的资源。

避免在Bitmap上浪费内存 读取一个Bitmap图片的时候,千万不要去加载不需要的分辨率。可以压缩图片等操作。

使用优化过的数据集合,Android提供了一系列优化过后的数据集合,如SparseArray,SparseBooleanArray,LongSparseArray,使用这些API可以让程序更加高效。HashMap工具类相对会比较低效,因为它需要为每一个键值对像提供一个对象入口,而SparseArray就避免了基本数据类型转换成对象数据类型的时间。

了解内存的开支情况

使用枚举类型比使用静态常量消耗两倍以上的内存,尽可能不使用枚举。

任何一个java类,包括匿名类,内部类,都要占用大概500字节的内存空间。

任何一个类的实例要消耗12--16字节的内存开支,因此频繁创建实例也是会在一定程度上影响内存的。

使用HashMap时,即使你只设置了一个基本数据类型的键,比如int,但是也会安照对象的大小类分配内存的。因此最好使用优化后的数据集合。

谨慎使用抽象编程,在Android使用抽象编程会带来额外的内存开支,因为抽象的编程,因为抽象的编程需要编写额外的代码,虽然这些代码根本执行不到,但是也要映射到内存中,不仅占用了更多的内存,在执行效率上也会有所降低。所以需要合理的使用抽象编程。

尽量避免使用依赖注入框架,使用依赖注入框架貌似看上去把findViewByid这一类的繁琐操作去掉了,但是这些框架为了要搜寻代码中的注解,通常需要经历较长的初始化过程,并且将一些你用不到的对象也并加载到内存中。这些用不到的对象会一直占用内存空间,可能过很久之后才 会得到释放。

使用多个进程,谨慎使用,多数应用程序不该在多个进程运行的,一旦使用不当,它甚至会增加额外的内存而不是帮我们节省内存。这个技巧比较适用于那些需要在后台去完成一项独立的任务,和前台是完全可以区分开的场景。比如音乐播放,关闭软件,已经完全由Service来控制音乐播放了,系统仍然会将许多UI方面的内存进行保留。在这种场景下就非常适合使用两个进程,一个用于UI展示,另一个用于在后台持续的播放音乐。关于实现多进程,只需要在Manifast文件的应用程序组件声明一个android:process属性就可以了。进程名可以自定义,但是之前要加个冒号,表示该进程是一个当前应用程序的私有进程。

分析内存的使用情况

系统不可能将所有的内存分配给我们的应用程序,每个程序都会有可能的内存上限,被称为堆大小。不同的手机的堆大小不同,如下代码可以获得堆大小:

ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
int heapSize = manager.getMemoryClass();

结果以MB为单位进行返回,我们开发时应用程序的内存不能超过这个限制。否则会出现oom。

Android的GC操作,Android系统会在适当的时机触发GC操作,一旦进行GC操作,就会将一些不再使用的对象进行回收。GC操作会从一个叫做Rooots的对象开始检查,所有它可以访问到的对象就说明还在使用中,应该进行保留,而其他的就表示已经不再使用了。

###Android中内存泄漏Android的垃圾回收机制并不能防止内存泄漏的出现导致内存泄漏最主要的原因就是某些长存在对象持有了一些其他应该被回收的对象的引用,导致垃圾回收器无法回收这些对象,也就是出现内存泄漏了。比如说像Activity这样的系统组件,它又会包含很多的控件甚至是图片,如果它无法被垃圾回收器回收掉的话,那就算是比较严重的内存泄漏情况了。举个例子,在MainActivity中定义一个内部类,实例化内部类对象,在内部类新建一个线程执行死循环,会导致内部类资源无法释放,MainActivity的控件和资源无法释放,导致OOM,可借助一系列工具,比如LeakCanary。

使用合适的算法和数据结构是优化程序性能的最主要手段。

避免创建不必要的对象,不必要的对象我们应该避免创建:

如果有需要拼接的字符串,那么可以优先考虑使用StringBuffer或者StringBuilder来进行拼接,而不是加号连接符,因为使用加号连接符会创建多余的对象,拼接的字符串越长,加号连接符性能越低。

在没有特殊的原因的情况下,尽量使用基本数据类型来代替封装数据类型,int比Integer要更加有效,其他数据类型也是一样。

当一个方法的返回值是String的时候,通常需要判断一下这个Sring的作用是什么,如果明确知道调用方法会将返回的String再进行拼接操作的话,可以考虑返回一个StringBuffer对象来代替,因为这样可以将一个对象的引用进行返回,而返回String的话就是创建了一个短生命周期的临时对象。

基本数据类型的数组也要由于对象数据类型的数组。另外两个平行的数组要比一个封装好的对象数组更加高效,举个例子:Fool[]和Bar[]这样的数组,使用起来要比Custom(Foo,Bar)[]这样的一个数组高效的多。

尽可能地少创建临时对象,越少的对象意味着越少的GC操作。

静态由于抽象,如果你并不需要访问一个对系那个中的某些字段,只是想调用某些方法去完成一项通用的功能,那么可以将这个方法设置为静态方法,调用速度提升15%到20%,同时也不用为了调用这个方法去专门创建对象了,也不用担心调用方法后是否会改变对象的状态(静态方法无法调用非静态字段)。

对常量使用static final修饰符

static int inVal = 42static String strVal= “Hello,world”;

编译器会为上面的代码生成一个初始方法,该方法会在定义类第一次被使用的时候调用。这个方法会将42的值赋值到intVal,从字符串常量表中提取一个引用赋值到strVal上。当赋值完成后,我们就可以通过字段搜寻的方式去访问具体的值了。

final进行优化:

static final int inVal = 42static final String strVal= “Hello,world”;

这样,定义类就不需要方法了,因为所有的常量都会在dex文件的初始器当中进行初始化。当我们调用intVal时可以直接指向42的值,而调用strVal会用一种相对轻量级的字符串常量方式,而不是字段搜寻的方式。

这种优化方式支持基本数据类型以及String类型的常量有效,对于其他数据类型的常量是无效的。

static class Counter {  
    int mCount;  
}  
  
Counter[] mArray = ...  
  
public void zero() {  
    int sum = 0;  
    for (int i = 0; i < mArray.length; ++i) {  
        sum += mArray[i].mCount;  
    }  
}  
  
public void one() {  
    int sum = 0;  
    Counter[] localArray = mArray;  
    int len = localArray.length;  
    for (int i = 0; i < len; ++i) {  
        sum += localArray[i].mCount;  
    }  
}  
  
public void two() {  
    int sum = 0;  
    for (Counter a : mArray) {  
        sum += a.mCount;  
    }  
}

zero最慢,one()相对快得多。 Tips:ArrayList手写的循环比增强for循环更快,其他组合没有这种情况。因为默认情况下使用增强型for循环,而遍历ArrayList使用传统的循环方式。

多使用系统封装好的Api

系统提供不了的Api完成不了我们需要的功能才应该自己去写,因为使用系统Api很多时候比我们自己写的代码要快的多,他们的很多功能都是通过底层的汇编模式执行的。举个例子,实现数组拷贝的功能,使用循环的方式对数组中的每一个元素一一进行赋值当然可行,但是直接使用系统中提供的System.arraycopy()方法会让执行效率快9倍以上。

避免在内部调用Getters/Settes方法

面向对象中封装的思想是不要把类内的字段暴露给外部,而是提供特定的方法来允许外部操作内部字段。但在Android中,字段搜寻要比方法调用效率要高的多,我们直接访问某个字段可能比通过getters方法来访问中国字段快3倍到7倍。但是编写代码还是要按照面向对象思维的,我们应该在能优化的地方进行优化,比如避免在内部调用getters/setters方法。

##布局优化技巧 重用布局文件,标签可以允许在一个布局当中引入另一个布局,那么比如说哦我们程序的所有界面都有一个公共的部分,这个时候最好的做法就是讲这个公共的部分提取到一个独立的布局中,然后每个界面的布局文件当中来引用这个公共的布局。

Tips:如果我们要在标签中复写Layout属性,必须要将Layout_width和Layout_height这两个属性也进行复写,否则复写效果将不会生效。

标签是作为一种辅助扩展来使用的,它的主要作用是为了防止在引用文件时产生多余的布局嵌套。布局嵌套越多,解析起来就越耗时,新能越差。因此编写布局文件时应该让嵌套的层数越少越好。这样可以减少层级嵌套,加快视图绘制渲染,提升UI性能。

仅在需要时才加载布局

某个布局当中的元素不是一起显示出来的,普通情况下只显示部分常用的元素,而那些不常用的元素只有在用户进行特定操作时才会显示出来。 用VISIBLE性能一般,可以用ViewStub。ViewStub也是View的一种,但是没有大小,没有绘制功能,也不参与布局,资源消耗非常低。

<ViewStub   
        android:id="@+id/view_stub"  
        android:layout="@layout/profile_extra"  
        android:layout_width="match_parent"  
        android:layout_height="wrap_content"  
        />  
public void onMoreClick() {  
    ViewStub viewStub = (ViewStub) findViewById(R.id.view_stub);  
    if (viewStub != null) {  
        View inflatedView = viewStub.inflate();  
        editExtra1 = (EditText) inflatedView.findViewById(R.id.edit_extra1);  
        editExtra2 = (EditText) inflatedView.findViewById(R.id.edit_extra2);  
        editExtra3 = (EditText) inflatedView.findViewById(R.id.edit_extra3);  
    }  
}

Tips:ViewStub所加载的布局是不可以使用标签的,因此这有可能导致加载出来出来的布局存在着多余的嵌套结构。

ListView详解,15年后,RecycleView盛行,不过ListView毕竟是前辈控件,了解了解。

ListView直接继承AbsListView,AbsListView继承自AdapterView,AdapterView继承自ViewGroup。

Adapter在ListView和数据源之间起到了一个桥梁的作用

RecycleBin机制

RecycleBin是listView能够实现成百上千的数据都不会OOM最重要的一个原因。RecycleBin是AbsListView的一个内部类。

RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews中。

mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中删除,下次获取同样位置的时候将会返回null,所以mActiveViews不能被重复利用。

addScrapView()用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃的时候(比如滚动出了屏幕)就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurentScrap这两个List来存储废弃View。

getScrapView用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。

我们都知道Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项单独启用一个RecycleBIn缓存机制。

View的流程分三步,onMeasure()用于测量View的大小,onLayout()用于确定view的布局,onDraw()用于将View绘制到界面上。

#RecycleView和ListView的异同 ViewHolder是用来保存视图引用的类,无论是ListView亦或是RecycleView。只不过在ListView中,ViewHolder需要自己来定义,且这只是推荐的使用方式,不使用当然也可以,这不是必须的,只不过不使用ViewHolder的话,ListView每次getView的时候都会调用findViewByid(int),这将导致ListView性能低效。而在RecycleView中使用RecycleView.ViewHolder则变成了必须,尽管,尽管实现起来稍显复杂,但它解决了ListView面临上述不使用自定义ViewHolder时所面临的问题。

我们知道ListView只能在垂直方向上滚动,Android API没有提供ListView在水平方向上面滚动支持。但是RecycleView相较于ListView,在滚动上面功能扩展了许多,它可以支持多种类型列表的展示要求,主要如下: 1 LinearLayoutManager,可以支持水平和竖直方向上滚动的列表。

2 StaggeredGridLayoutManager,可以支持瀑布流形式的列表。

3 GridLayoutManager,支持网络展示,可以水平或者竖直滚动,如展示图片的画廊。

列表动画是一个全新的、拥有无限可能的维度。起初的Android API中,删除或添加item时时无法产生动画效果的,后面随着Android的进化,Google的chat Hasse推荐使用ViewPropertyAnimator属性动画来实现上述需求。相比较于ListView,RecycleView.itemAnimator则被提供用于在RecycleView添加、删除或移动item时处理动画效果。同时,如果你比较懒,不想自定义itemAnimator,你还可以使用DefaultItemAnimator。

listView的Adapter中,getView是最重要的方法,它将视图跟position绑定起来,是所有神奇的事情发生的地方。同时我们也能够通过registeDataObserver就是这个观察者。ListView有三个Adapter的默认实现,分别是ArrayAdapter、CursorAdaptr和SimpleCursorAdapter。然而,RecycleView的Adapter则拥有除了内置的内DB游标和ArrayList的支持之外的所有功能。RecycleView.Adapter的实现的,我们必须采取措施将数据提供给Adapter,正如BaseAdapter对ListView所做的那样。

在ListView中如果我们想要在Item之间添加间隔符,我们只需要在布局文件中对ListView添加如下属性即可:

android:divider="@android:color/transparent"
  android:dividerHeight="5dp"

ListView通过AdapterView.OnItemClickListener接口来探测点击事件。而RecycleView则通过RecycleView.OnItemTouchListener接口来监听点击事件。它虽然增加了实现难度,但是却给予开发人员拦截触摸事件更多的控制权限。

ListView可以设置选择模式,并添加MultiChoicMOdeListener

listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);

AsyncTask

首先从Android3.0开始,系统要求网络访问必须在子线程中进行,否则网络访问将会失败并抛出 NetworkOnMainThreadException这个异常,这样做事为了避免主线程由于耗时操作所阻塞从而出现ANR现象。AsyncTask封装了线程池和Handler。AsyncTask有两个线程池:SerialExecutor和THREAD_POOL_EXECUTOR。前者用于任务的排队,默认是串行的线程池:后者用于真正的执行任务。AsyncTask还有一个Handler,叫InternaHandler,用于将执行环境从线程池切换到主线程。AsyncTask内部就是通过InternalHandler来发送任务执行的进度以及执行结束消息。

AsyncTask排队执行过程:系统先把参数Params封装为FutureTask对象,它相当于Runnable,接着FutureTask交给SerialExcutor的execute方法,它先把FutureTask对象插入到任务队列task中,如果这个时候没有正在活动的AsyncTask任务,那么就会执行下一个AsyncTask任务,同时当一个AsyncTask任务执行完毕之后,AsyncTask会继续执行其他任务直到所有任务都被执行为止。

关于线程池,AsyncTask对应的线程池ThreadPoolExecutor都是进程范围内共享的,都是static,所以是AsyncTask控制着进程范围内所有子类实例。由于总管限制的存在,当使用默认线程时,如果线程数超过线程池的最大容量,线程池就会爆掉。

Android动态加载dex技术实现

一综述

Android使用Dalvik虚拟机加载可执行程序,所以不能直接加载classdejar,而是需要将class转化为dex字节码,从而执行代码。优化后的字节码文件可以存在一个*.jar中,只有其内部存放的存放的是*.dex文件即可使用。

Android支持动态加载的两种方式是:DexClassLoader和PathClassLoader,DexClassLoader可加载jar/apk/dex,且支持从SD卡加载;PathClassLoader只能加载已经安装在Android系统内apk文件。

PathClassLoader的限制要更多一些,它只能加载已经安装到Android系统内的apk文件,也就是/data/app目录下的apk文件。其他位置的文件加载的时候都会出现ClassNotFoundException,例如:

PathClassLoader cl = new PathClassLoader(jarFile.toString(), "/data/app/", ClassLoader.getSystemClassLoader());

Android插件化基础:

开发者将代码封装成jar或者apk

宿主下载或者从本地加载jar或者apk到宿主中

将宿主调用插件中的算法或者Android特定的class(如Activity)

###插件化开发---动态加载技术加载已安装和未安装的apk

首先引入一个概念,动态加载技术是什么?为什么要引入动态加载?它有什么好处?

应用程序入手,大家都知道在Android app中,一个应用程序dex文件方法数最大不能超过65535个,否则,你的app将会出现异常了,那么如果越大的项目那肯定超过了,像美团、支持宝等都是使用动态加载技术,支付宝在去年的一个技术分享会议上就推崇让应用程序插件化,而美团也公布了他们的解决方案:Dex自动拆包和动态加载技术。所以使用动态加载技术解决此类问题。而它的优点可以让应用程序实现插件化对后期维护作用那就不用说了。

1 什么是动态加载技术?

动态加载技术就是使用类加载器加载相应的apk、dex、jar(必须含有dex文件),再通过反射获得该apk、dex、jar内部资源(class,图片,color等等)进而供宿主app使用。

2 关于动态加载使用的类加载器

使用动态加载技术时,一般需要用到这两个加载器:

PathClassLoader-只能加载已经安装到Android系统上的apk,即/data/app目录下的apk。

DexClassLoader-能加载手机未安装的apk、jar、dex,只要能找到相应的路径。

这两个加载器分别对应使用的场景各不同,所以接下来,分别讲解它们各自加载相同的插件apk的使用。

3 使用PathClassLoader加载已安装的apk插件,获取相应的资源供宿主app使用,下面通过一个demo来介绍PathClassLoader的使用:

(1)首先我们需要知道一个manifest中的属性:SharedUserId

该属性是干嘛的呢?简单的来说,应用从一开始安装在Android系统上时,系统都会给它分配一个userId,之后该应用在今后都将运行在独立的一个进程中,其他应用程序不能访问它的资源,那么如果两个应用的sharedUserId相同,那么它们将共同运行在相同linex进程中,从而便可以数据共享、资源访问了。所以我们在宿主app和插件app的manifest上都定义一个相同的sharedUserId。

(2)那么我们将插件apk安装在手机后,宿主app怎么知道手机内该插件是我们应用程序的插件呢?我们之前是不是定义过插件apk也是使用相同的sharedUserId,那么,我就可以这样思考了,是不是可以得到手机内存所有所有已经安装apk的sharedUserId呢,然后通过判断sharedUserId是否和宿主app的相同,如果是,那么该app就是我们的插件了,确实是这样的思路的,那么有了思路最大的问题就是怎么获取一个应用程序内的shareUserId了,我们可以通过PackageInfo.sharedUserId来获取,请看代码:

/**
     * 查找手机内所有的插件
     * @return 返回一个插件List
     */
    private List<PluginBean> findAllPlugin() {
        List<PluginBean> plugins = new ArrayList<>();
        PackageManager pm = getPackageManager();
        //通过包管理器查找所有已安装的apk文件
        List<PackageInfo> packageInfos = pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);
        for (PackageInfo info : packageInfos) {
            //得到当前apk的包名
            String pkgName = info.packageName;
            //得到当前apk的sharedUserId
            String shareUesrId = info.sharedUserId;
            //判断这个apk是否是我们应用程序的插件
            if (shareUesrId != null && shareUesrId.equals("com.sunzxyong.myapp") && !pkgName.equals(this.getPackageName())) {
                String label = pm.getApplicationLabel(info.applicationInfo).toString();//得到插件apk的名称
                PluginBean bean = new PluginBean(label,pkgName);
                plugins.add(bean);
            }
        }
        return plugins;
    }

通过这段代码,我们就可以轻松的获取手机内存在的所有插件,其中PluginBean是定义的一个实体类而已。

(3)如果找到了插件,就把可用的插件显示出来了,如果没有找到,那么就可提示用户先去下载插件什么的。

List<HashMap<String, String>> datas = new ArrayList<>();  
List<PluginBean> plugins = findAllPlugin();  
if (plugins != null && !plugins.isEmpty()) {  
    for (PluginBean bean : plugins) {  
        HashMap<String, String> map = new HashMap<>();  
        map.put("label", bean.getLabel());  
        datas.add(map);  
    }  
} else {  
    Toast.makeText(this, "没有找到插件,请先下载!", Toast.LENGTH_SHORT).show();  
}  
showEnableAllPluginPopup(datas);

(4)如果找到后,那么我们选择对应的插件时,在宿主app中就加载插件内对应的资源,这个才是PathClassLoader的重点:

/**
     * 加载已安装的apk
     * @param packageName 应用的包名
     * @param pluginContext 插件app的上下文
     * @return 对应资源的id
     */
    private int dynamicLoadApk(String packageName, Context pluginContext) throws Exception {
        //第一个参数为包含dex的apk或者jar的路径,第二个参数为父加载器
        PathClassLoader pathClassLoader = new PathClassLoader(pluginContext.getPackageResourcePath(),ClassLoader.getSystemClassLoader());
//        Class<?> clazz = pathClassLoader.loadClass(packageName + ".R$mipmap");//通过使用自身的加载器反射出mipmap类进而使用该类的功能
        //参数:1、类的全名,2、是否初始化类,3、加载时使用的类加载器
        Class<?> clazz = Class.forName(packageName + ".R$mipmap", true, pathClassLoader);
        //使用上述两种方式都可以,这里我们得到R类中的内部类mipmap,通过它得到对应的图片id,进而给我们使用
        Field field = clazz.getDeclaredField("one");
        int resourceId = field.getInt(R.mipmap.class);
        return resourceId;
    }

这个方法就是加载包名为packageName的插件,然后获得插件内名为one.png的图片的资源id,进而供宿主app使用该图片。

首先就是new出一个PathClassLoader对象,它的构造方法为:

public PathClassLoader(String dexPath, ClassLoader parent)

其中第一个参数就是通过插件的上下文来获取插件apk的路径,其实获取到的就是/data/app/apkthemeplugin.apk,那么插件的上下文字幕获取呢?在宿主app中只有本app的上下文啊,答案就是为插件app创建一个上下文:

//获取对应插件中的上下文,通过它可得到插件的Resource
Context plugnContext = this.createPackageContext(packageName,CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_DODE);

通过插件的包名来创建上下文,不过这种方法只适合获取已安装的app的上文,或者不需要通过反射直接通过插件上下文getResource().getxxx(R..);也行,而这里用的是反射方法。

第二个参数是父加载器,都是ClassLoader.getSystemClassLoader()。

好了,插件app的类加载器我们创建出来了,接下来就是通过反射获取对应类的资源了,这里我是获取R类中的内部类mipmap类,然后通过反射得到mipmap类中名为one的字段的值。

就可以获取对应id的Drawable得到该图片资源进而宿主app可以使用了。

关于R类,在AS中目录为:/build/generated/source/r/debug/<-packageName->.

可不可以只下载下来,不用安装,也能提供宿主app使用呢?这就需要用到另一个加载器DexClassLoader。

DexClassLoader加载未安装的apk,提供资源供宿主app使用

关于动态加载未安装的apk,首先我们得到事先知道我们的插件apk存放在哪个目录下,然后分别得到插件apk的信息(名称、包名等),然后显示可用的插件,最后动态加载apk获取资源。

按照上面这个思路,我们需要解决几个问题: 1、怎么得到未安装的apk信息

2、怎么得到插件的context或者Resource,因为它是未安装的不可能通过createPackageContext(xxx);方法来构建出一个Context,所以这时只有在Resource上下功夫。

(1)得到未安装的apk信息可以通过mPackageManager.getPackageArchiveInfo()方法获得,

public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags)

它的参数刚好是传入一个FilePath,然后返回apk文件的PackageInfo信息:

/**
     * 获取未安装apk的信息
     * @param context
     * @param archiveFilePath apk文件的path
     * @return
     */
    private String[] getUninstallApkInfo(Context context, String archiveFilePath) {
        String[] info = new String[2];
        PackageManager pm = context.getPackageManager();
        PackageInfo pkgInfo = pm.getPackageArchiveInfo(archiveFilePath, PackageManager.GET_ACTIVITIES);
        if (pkgInfo != null) {
            ApplicationInfo appInfo = pkgInfo.applicationInfo;
            String versionName = pkgInfo.versionName;//版本号
            Drawable icon = pm.getApplicationIcon(appInfo);//图标
            String appName = pm.getApplicationLabel(appInfo).toString();//app名称
            String pkgName = appInfo.packageName;//包名
            info[0] = appName;
            info[1] = pkgName;
        }
        return info;
    }

(2)得到对应未安装apk的Resource对象,我们需要通过反射来获得:

/**
     * @param apkName 
     * @return 得到对应插件的Resource对象
     */
    private Resources getPluginResources(String apkName) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);//反射调用方法addAssetPath(String path)
            //第二个参数是apk的路径:Environment.getExternalStorageDirectory().getPath()+File.separator+"plugin"+File.separator+"apkplugin.apk"
            addAssetPath.invoke(assetManager, apkDir+File.separator+apkName);//将未安装的Apk文件的添加进AssetManager中,第二个参数为apk文件的路径带apk名
            Resources superRes = this.getResources();
            Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),
                    superRes.getConfiguration());
            return mResources;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

其中new DexClassLoader()来插件未安装apk的类加载器。我们来看看参数:

public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

dexPath就是apk文件的路径,optimizedDirectory - apk解压缩后的存放dex的目录,值得注意的是,在4.1以后该目录不允许在sd卡上,所以我们用getDir()方法在应用内部创建一个dexOutputDir。

libraryPath --本地的Library,一般为null

parent--父加载器

接下来,就是通过反射的方法,获取相应的资源

什么是动态加载技术:就是使用类加载器加载相应含有dex文件的apk、dex、jar,再通过反射获取该apk,jar,dex中资源进入供宿主app使用。

插件化技术学习

各大厂商都碰到了AndroidNative平台的瓶颈:

1 从技术山来讲,业务逻辑的复杂代码急剧膨胀同时对模块热更新提出了更高的要求。

2 从业务层面上,功能模块的解耦以及维护团队的分离也是大势所趋。

插件哈技术主要解决两个问题

1 代码加载,2 资源加载

代码加载,类的加载可以使用java 的ClassLoader机制,还需要组件生命周期管理。

资源加载 用AssetManager的隐藏方法addAssetPath。

Android插件化原理即解析---Hook机制之动态代理

使用代理机制进行API Hoook进行进而达到方法增强。

动态代理,可以简单理解为jvm可以在运行时帮我们动态生成一系列的代理类。

代理Hook

如果我们直接创建代理对象,然后把原始对象替换为我们的代理对象,就可以在这个代理对象中为所欲为了;修改参数,替换返回值,称为hook。

整个hook过程简要总结如下:

1寻找Hook点,原则是静态变量或者单例对象,尽量Hook public的对象和方法,非public不保证每个版本都一样,需要适配。

2选择合适的代理方式,如果是接口可以用动态代理;如果是类可以手动写代理也可以使用cglib。 偷梁换柱-用代理对象替换原始对象 ##Android插件化原理解析——Hook机制之Binder Hook

Android事件分发:在Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟悉整套的分发机制有助于更好的分析各种点击滑动问题,更好去扩展控件的事件功能和开发自定义控件,同时事件分发机制也是Android面试必问考点之一,如果你能把下面的一些事件分发图当场画出来肯定加分不少。

关于Android事件分发机制网上的博文很多,但是很多都是写个Demo然后贴一下输出的Log或者拿源码分析,然后一堆的注释和说明,如果用心的去看肯定是收货不少但是确实很难把整个流程说清楚和记住。

image

仔细的话,图分为3层,从上往下依次是Activity,ViewGroup,View

事件由Activity的dispatchTouchEvent做分发。箭头上面字代表方法返回值,super的意思是调用父类实现。

dispatchTouchEvent和onTouchEvent的框里有个【true--->】的字,表示的意思是如果方法返回true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。目前所有图的事件是针对ACTION_DOWN,对于ACTION_MOVE和ACTION_UP我们最后做分析。

仔细看整个图,我们得出事件流走向的几个结论:

1、如果事件不被中断,整个事件流向是一个类U型图

image

所以如果我们没有对控件里面的方法进行重写或更改返回值,而直接用super调用父类的默认实现,那么整个事件流向应该是从Activity---->ViewGroup-->View从上往下调用dispatchTouchEvent方法,一直到叶子节点(View)的时候,再由View--->ViewGroup--->Activity从下往上调用onTouchEvent方法。

2 dispatchTouchEvent 和onTouchEvent 一旦return true,事件就停止传递了(到达终点)。看上图图中只要return ture事件就没有再继续传下去了,对于return true 我们经常说事件被消费了,消费了的意思就是事件走到这里就是终点,不会往下传,没有谁再能收到这个事件了。对于返回false的情况,事件都是传给父控件onTouchEvent处理。

对于dispatchTouchEvent返回false的含义应该是:事件停止往子View传递和分发同时开始往父控件回溯(父控件的onTouchEvent开始从下往上回传直接到某个onTouchEvent return true),事件分发机制就像递归,return false的意义就是递归停止然后开始回溯(父控件的onTouchEvent开始从下往上回传直到某个onTouchEvent return true),事件分发机制就像递归,return false 的意义就是递归停止然后开始回溯。

对于onTouchEvent return false就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动。

dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent ,ViewGroup和View的这些方法的默认实现就是会让整个事件安装UI型完整走完,return super.xxx()就会让事件依照UI型的方向的完整走完整个事件流动路径,中间不做任何改动,不回溯,不终止,每个环节都走到。

link

所以如果看到方法return super.xxxx()那么事件的下一个流向就是走U型下一个目标,稍微记住上面这张图,你就能很快判断出下一个走向是哪个控件的哪个函数。

onInterceptTouchEvent的作用

image

Intercept的意思就拦截,每个ViewGroup每次做分发的时候,问一问拦截器要不要拦截(也就是问问自己这个事件要不要自己来处理)如果要自己处理那就在onInterceptTouchEvent方法中return true就会交给自己的onTouchEvent的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,因为子View也需要这个事件,所以onInterceptTouchEvent拦截器return super.onINterceptTouchEvent()和return false是一样的,是不会拦截的,事件会继续往子View的dispatchTouchEvent传递。

ViewGroup和View的dispatchTouchEvent方法返回super.dispatchTouchE vevnt()的时候事件流走向。

image

首先看ViewGroup的dispatchTouchEvent,之前说的return true是终结传递。return false是回溯父 View的onTouchEvent,然后ViewGroup怎样通过dispatchTouchEvent方法能把时间分发到自己的onTouchEvent处理呢?return true和false都不行,那么只能通过Interceptor把事件拦截下来给自己的onTouchEvent,所以ViewGroup dispatchTouchEvent方法的super默认实现就是去调用onInterceptTouchEvent,记住这一点。那么对于View的dispatch TouchEvent return super.dispatch TouchEvent()的时候事件会传递到哪,很遗憾View没有拦截器,但是同样的道理return true是终结。return false 是回溯到父类的onTouchEvent,怎样把事件分发给自己的onTouchEvent处理呢?那只能return super.dispatchTouchEvent,View类的dispatchTouchEvent()方法默认实现就是能帮你调用View自己的onTouchEvent方法的。

对于dispatchTouchEvent,onTouchEvent,retrun true是终结事件传递。return false是回溯到父View的onTouchEvent方法。

ViewGroup想把自己分发给自己的onTouchEvent,需要拦截器onInterceptTouchEvent方法return true把事件拦截下来。

ViewGroup的拦截器默认是不拦截的,所以return super.onInterceptTouchEvent()== retrun false;

View没有拦截器,为了让view可以吧事件分发给自己的onTouchEvent,View的dispatchToucheEvent默认实现(super)就是把事件分发给自己的OnTouchent。

ViewGroup和View的dispatchTouchEvent是做事件分发,那么这个事件可能分发出去的四个目标

注:-----后面代表事件目标需要怎么做。

1 自己消费,终结传递-----return true;

2 给自己的onTouchEvent处理----调用super.dispatchTouchEvent()系统默认会去调用onInterceptTouchEvent,在onInterceptTouchEvent return true就会去把事件分给自己的onTouch处理。

3 传给子View---->调用super.dispatchTouchEvent()默认实现会去调用onInterceptTouchEvnet在onInterceptTouchEvent()默认实现会去调用onInterceptTouchEvent在onInterceptTouchEvent return false,就会把事件传给子View了。

4 不传给子View,事件终止往下传递,事件开始回溯,从父View的onTouchEvent开始事件从下到上回归执行每个控件的onTouchEvent---->return false;

注: 由于View没有子View所以不需要onInterceptTouchEvent 来控件是否把事件传递给子View还是拦截,所以View的事件分发调用super.dispatchTouchEvent()的时候默认把事件传给自己的onTouchEvent处理(相当于拦截),对比ViewGroup的dispatchTouchEvent 事件分发

ViewGroup和View的onTouchEvent方法是做事件处理的,那么这个事件只能有两个处理方式:

1 自己消费掉,事件终结,不再传给谁---return true;

2 继续从下往上传,不消费事件,让父View也能收到这个事件----return false;View的默认实现是不消费的。所以super==false。

ViewGroup的onInterceptTouchEvent方法对于事件有两种情况:

1 拦截下来,给自己的onTouchEvent处理---return ture;

2 不拦截,把事件往下传给子View----return false,ViewGroup默认是不拦截的,所以super==false;

关于ACTION_MOVE和ACTION_UP ####

上面讲解的都是针对ACTION_DOWN的事件传递,ACTION_MOVE和ACTION_UP在传递过程中并不是和ACTION_DOWN一样,你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个事件(如ACTION_DOWN)返回true,才会收到ACTION_MOVE和ACTION_UP事件。具体这句话很多博客都说了,但是具体含义是什么呢?

上面提到过了,事件如果不被打断的话,是会不断传给子View,然后不断回传到Activity,dispatchTouchEvent和onTouchEvent可以通过return true消费事件,分流导流的作用,ACTION_MOVE和ACTION_UP会在哪些函数被调用,之前说了并不是哪个函数收到了ACTION_DOWN,就会收到ACTION_MOVE等后续的事件的。下面通过几张图看看不同场景下,ACTION_MOVE和ACTION_UP事件的具体走向并总结一下规律。

1 我们在ViewGroup1的dispatchTouchEvent方法返回true消费这次事件

ACTION_DOWN事件从(Activity的dispatchTouchEvent)---->(ViewGroup1的dispatchTouchEvent)后结束传递,事件被消费(如下图红色的箭头代码ACTION_DOWN事件的流向)。

//打开日志
Activity  | dispatchTouchEvent-->ACTION_DOWN
ViewGroup | dispatchTouchEvent-->AcTION_DOWN

----->消费

image

在这种场景下ACTION_MOVE和ACTION_UP将如何呢?看下面的打出来的日志

Activity | dispatchTouchEvent --> ACTION_MOVE 
ViewGroup1 | dispatchTouchEvent --> ACTION_MOVE
----
TouchEventActivity | dispatchTouchEvent --> ACTION_UP 
ViewGroup1 | dispatchTouchEvent --> ACTION_UP
----

下图红色的箭头代表ACTION_DOWN事件的流向,蓝色的箭头代表ACTION_MOVE和ACTION_UP事件的流向

image

2 我们在ViewGroup2的dispatchTouchEvent返回true消费这次事件

Activity | dispatchTouchEvent --> ACTION_DOWN 
ViewGroup1 | dispatchTouchEvent --> ACTION_DOWN
ViewGroup1 | onInterceptTouchEvent --> ACTION_DOWN
ViewGroup2 | dispatchTouchEvent --> ACTION_DOWN
---->消费
Activity | dispatchTouchEvent --> ACTION_MOVE 
ViewGroup1 | dispatchTouchEvent --> ACTION_MOVE
ViewGroup1 | onInterceptTouchEvent --> ACTION_MOVE
ViewGroup2 | dispatchTouchEvent --> ACTION_MOVE
----
TouchEventActivity | dispatchTouchEvent --> ACTION_UP 
ViewGroup1 | dispatchTouchEvent --> ACTION_UP
ViewGroup1 | onInterceptTouchEvent --> ACTION_UP
ViewGroup2 | dispatchTouchEvent --> ACTION_UP
----

3 我们在View的dispatchTouchEvent返回true消费这次事件

效果和在ViewGroup2的dispatchTouchEvent return true的差不多,同样的收到ACTION_DOWN的dispatchTouchEvent函数都能收到ACTION_MOVE和ACTION_UP。

4 我们在View的onTouchEvent返回true消费这次事件

红色的箭头代表ACTION_DOWN 事件的流向,蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image

5 我们在ViewGroup2的onTouchEvent返回true消费这次事件

红色的箭头代表ACTION_DOWN事件流向,蓝色箭头代表ACTON_MOVE事件流向

image

6 我们在ViewGroup1的onTouchEvent返回true消费这次事件

红色的箭头代表ACTION_DOWN 事件的流向

蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image

8、我们在View的dispatchTouchEvent 返回false并且Activity 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image

9、我们在View的dispatchTouchEvent 返回false并且ViewGroup 1 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image

10、我们在View的dispatchTouchEvent 返回false并且在ViewGroup 2 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image

11、我们在ViewGroup2的dispatchTouchEvent 返回false并且在ViewGroup1 的onTouchEvent返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image

12、我们在ViewGroup2的onInterceptTouchEvent 返回true拦截此次事件并且在ViewGroup 1 的onTouchEvent返回true消费这次事件。
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image

相信已经看出规律了,对于在onTouchEvent消费事件的情况:在哪个View的onTouchEvent返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给这个View的onTouchEVent并结束本次事件传递过程。

对于ACTION_MOVE、ACTION_UP总结:ACTION_DOWN事件在哪个控件消费了(return true),那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)着落事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。

ANR

1 ANR排错一般有三种类型

(1)主要是物理按键或触摸事件在特定时间内无响应。

(2)BroadcastReceiver在特定时间内无法处理完成

(3)Service在特定的时间内无法处理完成

2 哪些操作会导致ANR在主线程执行以下操作:

(1)高耗时操作,如图像变换,大量循环

(2)磁盘读写,数据库读写操作

(3)大量的创建新对象

3 如何避免

(1)UI线程尽量只做跟UI相关的工作

(2)耗时的操作(比如数据库操作,I/O,连接网络或者有可能阻塞UI线程的操作)把它放到单独的线程中处理。

(3)尽量用Handler来处理UIThread和子线程的交互。

4 解决的逻辑

1 使用AsyncTask

1)在doInBackGround()方法中执行耗时操作

(2)在onPostExecuted()更新UI

2 使用Handler实现异步任务

1)在子线程中处理耗时操作
 
 (2)处理完成之后,通过handler.sendMessage()传递处理结果
 
 (3)在handler的handleMessage()方法中更新UI
 
 (4)或者使用handler.post()方法将消息放到Looper中

5 如何排查

1)首先分析log2)看代码
 
 (3)仔细查看ANR的成因
 
 (4)监测ANR的log比如使用LeakCanary

#ART和Dalvik区别

art应用启动快,运行快,但是耗费更多存储空间,安装时间长,总的来说art的功效就是“空间换时间”。

什么是Dalvik:Dalvik是Google公司自己设计用于Android平台的java虚拟机。Dalvik虚拟机是Google等厂商合作开发的Android移动设备平台的核心组成部分之一,它可以支持已为.dex格式的java应用程序的运行,.dex 格式是专为Dalvik应用设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik经过优化,允许在有限的内存 中同时运行多个虚拟机的实例,并且每一个Dalvik应用作为独立的Linux进程执行。独立的进程可以防止在在虚拟机崩溃的时候所有的程序都被关闭。

什么是ART:Android操作系统已经成熟,Google的Android团队开始将注意力转向底层组件,其中之一是负责应用程序运行的Dalvik运行时。Google开发者已经花了两年时间开发更快更高更省电的ART运行时。ART代表Android Runtime,其处理应用执行的方式完全不同于Dalvik,Dalvik是依靠一个JUST-in-Time(JIT)编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同的硬件和架构上运行。

ART则完全改变了这套做法,在应用安装的时候就预编译字节码到机器语言,这一机制叫Ahead-Of-Time(AOT)编译。在移除解释代码这一过程后,应用程序执行将会更有效率,启动更快。

ART优点:

1 系统性能的显著提升

2 应用启动和运行速度更快,触感反馈更及时。

3 更长的电池续航能力

4 支持更低的硬件

缺点:

1 需要更大的存储空间

2 更长的应用安装时间

#Android关于OOM的解决方案

1 内存溢出(Out Of Memory)

2 也就是说内存占有量超过了VM所分配的最大

出现OOM的原因

1 加载对象过大

2 相应资源过多,来不及释放

如何解决

1 在内存引用上做些处理,比如常用的软引用、强引用、弱引用

2 在内存中加载图片时直接在内存中作处理,如图片压缩。

3 动态回收内存

Fragment 为何产生

同时适配手机和平板、UI和逻辑的共享。

介绍

Fragment也会加入回退栈中

Fragment也会被加入回退栈中。

Fragment拥有自己的生命周期同时接受和处理用户事件

可以动态添加、替换和移除某个Fragment。

#生命周期

必须依附于Activity

image

Fragment依附于Activity的生命状态

image

##Fragment生命周期方法的含义:

public void onAttach(Context context)

onAttach方法会在Fragment于窗口关联后立刻调用。从该方法开始,就可以通过Fragment.getActivity方法获取与Fragment关联的窗口对象,但因为Fragment的控件未初始化,所以不能操作控件。

public void onCreate(Bundle savedInstanceState)

在调用完onAttach执行完之后立刻调用onCreate方法,可以在Bundle对象中获取一些在Activity中传过来的数据。通常会在该方法中读取保存的状态,获取或初始化一些数据。在在该方法中不要做耗时操作。

public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState)

该方法是Fragment很重要的一个生命周期方法,因为会在该方法中创建Fragment显示的View,其中inflater是用来装载布局文件的,container是<fragment>标签对应对象,savedinstanceStae参数可以获取Fragment保存的状态,如果未保存那么就为null。

public void onViewCreated(View view,Bundle savedInstanceState)

Android在创建完Fragment中的View对象之后,会立刻回调该方法。其中view参数就是onCreateView中返回的view。而bundle对象用于一般用途。

public void onActivityCreated(Bundle savedInstanceState)

在Activity的onCreate方法执行完之后,Android系统会立刻调用该方法,表示窗口已经初始化完成,从这一个时候开始,就可以在Fragment中使用getActivity().findViewByid(id)

public void onStop()

当onStop返回的时候,fragment将从屏幕上消失。

public void onDestoryView()

该方法的调用意味着在 onCreateView 中创建的视图都将被移除。

public void onDestroy()

Android在Fragment不再使用时会调用该方法,要注意的是这时Fragment还和Activity藕断丝连!并且可以获得Fragment对象,但无法对获得的Fragment进行任何操作

public void onDetach()

为Fragment生命周期中的最后一个方法,当该方法执行完后,Fragment与Activity不再有关联

Fragment比Activity多了几个额外的生命周期回调方法:

onAttach(Activity):当Fragment和Activity发生关联时使用 onCreateView(LayoutInflater, ViewGroup, Bundle):创建该Fragment的视图 onActivityCreate(Bundle):当Activity的onCreate方法返回时调用 onDestoryView():与onCreateView相对应,当该Fragment的视图被移除时调用 onDetach():与onAttach相对应,当Fragment与Activity关联被取消时调用

注意:除了onCreateView,其他的所有方法如果你重写了,必须调用父类对于该方法的实现

Fragment状态的持久化。由于Activity会经常性的发生配置变化,所以依附它的Fragment就有需要将其状态保存起来问题。下面有两个常用的方法去将Fragment的状态持久化。

方法一:

可以通过protected void onSaveInstanceState(Bundle outState),protected void onRestoreInstanceState(Bundle savedInstanceState) 状态保存和恢复的方法将状态持久化。

方法二(更方便,让Android自动帮我们保存Fragment状态):

我们只需要将Fragment在Activity中作为一个变量整个保存,只要保存了Fragment,那么Fragment的状态就得到保存了,所以呢..... FragmentManager.putFragment(Bundle bundle, String key, Fragment fragment) 是在Activity中保存Fragment的方法。 FragmentManager.getFragment(Bundle bundle, String key) 是在Activity中获取所保存的Frament的方法。

很显然,key就传入Fragment的id,这就意味着FragmentManager是通过Bundle去保存Fragment的。但是,这个方法仅仅能够保存Android将不会为我们保存控件的状态,比如说EditText中用户已经输入的文字(注意!在这里,控件需要设置一个id,否则Android将不会为我们保存控件的状态),而Fragment中需要持久化的变量依然会丢失,但依然有解决办法,就是利用方法一!

下面给出状态持久化的事例代码:
 	 /** Activity中的代码 **/
 	 FragmentB fragmentB;

     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.fragment_activity);
         if( savedInstanceState != null ){
             fragmentB = (FragmentB) getSupportFragmentManager().getFragment(savedInstanceState,"fragmentB");
         }
         init();
     }

     @Override
     protected void onSaveInstanceState(Bundle outState) {
         if( fragmentB != null ){
             getSupportFragmentManager().putFragment(outState,"fragmentB",fragmentB);
         }

         super.onSaveInstanceState(outState);
     }

     /** Fragment中保存变量的代码 **/

     @Nullable
     @Override
     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
         AppLog.e("onCreateView");
         if ( null != savedInstanceState ){
             String savedString = savedInstanceState.getString("string");
             //得到保存下来的string
         }
         View root = inflater.inflate(R.layout.fragment_a,null);
         return root;
     }

 	@Override
     public void onSaveInstanceState(Bundle outState) {
         outState.putString("string","anAngryAnt");
         super.onSaveInstanceState(outState);
     }

静态使用Fragment

1 继承Fragment,重写onCreateView决定Fragment的布局

2 在Activity中声明此Fragment,就和普通的View一样

##Fragment常用的API

android.support.v4.app.Fragment 主要用于定义Fragment

android.support.v4.app.FragmentManager 主要用于在Activity中操作Fragment,可以使用FragmentManager.findFragmenById,FragmentManager.findFragmentByTag等方法去找到一个Fragment

android.support.v4.app.FragmentTransaction

保证一些列Fragment操作的原子性,熟悉事务这个词

主要的操作都是FragmentTransaction的方法

(一般我们为了向下兼容,都使用support.v4包里面的Fragment)

getFragmentManager() // Fragment若使用的是support.v4包中的,那就使用getSupportFragmentManager代替

主要的操作都是FragmentTransaction的方法
	
	FragmentTransaction transaction = fm.benginTransatcion();//开启一个事务
	transaction.add() 
	//往Activity中添加一个Fragment

	transaction.remove() 
	//从Activity中移除一个Fragment,如果被移除的Fragment没有添加到回退栈(回退栈后面会详细说),这个Fragment实例将会被销毁。

	transaction.replace()
	//使用另一个Fragment替换当前的,实际上就是remove()然后add()的合体~

	transaction.hide()
	//隐藏当前的Fragment,仅仅是设为不可见,并不会销毁

	transaction.show()
	//显示之前隐藏的Fragment

	detach()
	//当fragment被加入到回退栈的时候,该方法与*remove()*的作用是相同的,
	//反之,该方法只是将fragment从视图中移除,
	//之后仍然可以通过*attach()*方法重新使用fragment,
	//而调用了*remove()*方法之后,
	//不仅将Fragment从视图中移除,fragment还将不再可用。

	attach()
	//重建view视图,附加到UI上并显示。

	transatcion.commit()
	//提交一个事务
	
	
	
	##管理Fragment回退栈

跟踪回退栈状态

我们通过实现*OnBackStackChangedListener*接口来实现回退栈状态跟踪,具体如下
 public class XXX implements FragmentManager.OnBackStackChangedListener 

 /** 实现接口所要实现的方法 **/

 @Override
 public void onBackStackChanged() {
     //do whatevery you want
 }

 /** 设置回退栈监听接口 **/
 getSupportFragmentManager().addOnBackStackChangedListener(this);
 
 
 管理回退栈

FragmentTransaction.addToBackStack(String) --将一个刚刚添加的Fragment加入到回退栈中
getSupportFragmentManager().getBackStackEntryCount() -获取回退栈中实体数量
getSupportFragmentManager().popBackStack(String name, int flags) -根据name立刻弹出栈顶的fragment
getSupportFragmentManager().popBackStack(int id, int flags) -根据id立刻弹出栈顶的fragment

为什么要使用SurfaceView来实现动画?因为View的绘图存在以下缺陷:

1 View缺乏双缓冲机制

2 当程序需要更新View上图像时,程序必须重绘View上显示的整张图片

3 新线程无法直接更新View组件

SurfaceView的绘图机制

一般会与SurfaceView结合

调用SurfaceView的getHolder()方法即可获得SurfaceView关联的SurfaceHolder

SurfaceHolder提供了如下方法来获取Canvas对象

1 Canvas lockCanvas():锁定整个SurfaceView对象,获取该Surface上的Canvas

2 Canvas lockCanvas(Rect dirty):锁定SurfaceView上Rect划分的区域,获取该Surface上Canvas

3 unlockCanvasAndPost(canvas):释放绘图,提交所绘制的图形,需要注意,当调用SurfaceHolder上的unlockCanvasAndPost方法之后,该方法之前所绘制的图形还处于缓冲之中,下一次lockCanvas方法锁定的区域可能会“遮挡”它

public class SurfaceViewTest extends Activity
{
	// SurfaceHolder负责维护SurfaceView上绘制的内容
	private SurfaceHolder holder;
	private Paint paint;

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		paint = new Paint();
		SurfaceView surface = (SurfaceView) findViewById(R.id.show);
		// 初始化SurfaceHolder对象
		holder = surface.getHolder();
		holder.addCallback(new Callback()
		{
			@Override
			public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2,
					int arg3)
			{
			}

			@Override
			public void surfaceCreated(SurfaceHolder holder)
			{
				// 锁定整个SurfaceView
				Canvas canvas = holder.lockCanvas();
				// 绘制背景
				Bitmap back = BitmapFactory.decodeResource(
					SurfaceViewTest.this.getResources()
					, R.drawable.sun);
				// 绘制背景
				canvas.drawBitmap(back, 0, 0, null);
				// 绘制完成,释放画布,提交修改
				holder.unlockCanvasAndPost(canvas);
				// 重新锁一次,"持久化"上次所绘制的内容
				holder.lockCanvas(new Rect(0, 0, 0, 0));
				holder.unlockCanvasAndPost(canvas);
			}

			@Override
			public void surfaceDestroyed(SurfaceHolder holder)
			{
			}
		});
		// 为surface的触摸事件绑定监听器
		surface.setOnTouchListener(new OnTouchListener()
		{
			@Override
			public boolean onTouch(View source, MotionEvent event)
			{
				// 只处理按下事件
				if (event.getAction() == MotionEvent.ACTION_DOWN)
				{
					int cx = (int) event.getX();
					int cy = (int) event.getY();
					// 锁定SurfaceView的局部区域,只更新局部内容
					Canvas canvas = holder.lockCanvas(new Rect(cx - 50,
							cy - 50, cx + 50, cy + 50));
					// 保存canvas的当前状态
					canvas.save();
					// 旋转画布
					canvas.rotate(30, cx, cy);
					paint.setColor(Color.RED);
					// 绘制红色方块
					canvas.drawRect(cx - 40, cy - 40, cx, cy, paint);
					// 恢复Canvas之前的保存状态
					canvas.restore();
					paint.setColor(Color.GREEN);
					// 绘制绿色方块
					canvas.drawRect(cx, cy, cx + 40, cy + 40, paint);
					// 绘制完成,释放画布,提交修改
					holder.unlockCanvasAndPost(canvas);
				}
				return false;
			}
		});
	}
	}
	
	
	上面的程序为SurfaceHolder添加了一个CallBack实例,该Callback中定义了如下三个方法:

void surfaceChanged(SurfaceHolder holder, int format, int width, int height):当一个surface的格式或大小发生改变时回调该方法。
void surfaceCreated(SurfaceHolder holder):当surface被创建时回调该方法
void surfaceDestroyed(SurfaceHolder holder):当surface将要被销毁时回调该方法

#Android几种进程

1 前台进程:即与用户正在交互的Activity或者Activity用到的Service等,如果系统内存不足时前台进程是最后被杀死的。

2 可见进程:可以是处于暂停状态(onPause)的Activity或者绑定在其上的Service,即被用户可见,但由于失去了焦点而不能与用户交互

3 服务进程:其中运行着使用startService方法启动的Service,虽然不被用户可见,但是却是用户关心的,例如用户正在非下载页面自己下载的文件和非音乐界面听的音乐等;当内存不足时,前两者进程才会被终止。

4 后台进程:其中运行着执行onStop方法而停止的程序,但是却不是用户当前关心的,这样的进程系统一旦没有了内存就首先被杀死。

5 空进程:不包含任何应用程序的程序组件的进程,这样的进程系统一般不会让他存在的。

如何避免后台进程被杀死:

1 调用startForegound,让你的Service所在的线程成为成为前台进程

2 Service的onStartCommond返回START_STICKY或者START_REDELIVER_INTENT

3 Service的onDestroy里面重新启动自己

#APP启动过程

image

上图可以很好的说明App启动的过程

ActivityManagerService组织回退栈时以ActivityRecord为基本单位,所有的ActivityRecord放到ArrayList里面,可以将mHistory看作一个栈对象,索引0所指的对象位于栈低,索引mHistory.size()-1所指的对象位于栈顶。

Zygote进程孵化出新的应用进程后,会执行ActivityThread类的main方法,在该方法里会先准备好Looper和消息队列,然后调用attach方法将应用进程绑定到ActivityManagerService,然后进入Loop循环,不断地读取消息队列里的消息,并分发消息。

ActivityThread 的main方法执行后,应用进程接下来通知ActivityManagerService应用进程已启动,ActivityManagerService保存应用进程的一个代理对象,这样ActivityManagerService可以通过这个代理对象控制应用进程,然后ActivityManagerService通知应用进程创建入口Activity的实例,并执行它的生命周期方法。

#Android图片中的三级缓存

为什么要使用三级缓存

1 如今的Android App经常需要网络交互,通过网络获取图片是正常不过的事了

2 提出三级缓存策略,更快显示图片,提高用户体验,通过网络、本地、内存三级缓存图片,来减少不必要的网络交互,避免过长的时间等待。

什么是三级缓存

1 网络加载,不优先加载,速度慢,浪费流量,影响用户体验

2 本地缓存,次优先加载,速度快

3 内存缓存,优先加载,速度最快

三级缓存原理

首先加载Android App时,肯定要通过网络交互来获取图片,之后我们可以将图片保存至本地SD卡和内存中

总之,只在初次访问新内容时,才通过网络获取图片资源

热修复技术

App提早发出去的包,如果出现客户端的问题,实在是干着急,覆水难收。因此线上修复方案就出现了。

概述

基于Xposed中的思想,通过修改c层的Method实例描述,来实现更改与之对应的java的行为,从而达到修复的目的。

Xposed

诞生于XDA论坛,类似一个应用平台,不同的是提供诸多系统级的应用。可实现许多神奇的功能。Xposed需要以越狱为前提,像是ios中的cydia。

###Android系统启动与应用启动

Zygote进程是Android手机系统启动后,常驻的一个名为‘受精卵’的进程。

zygote的启动实现脚本在 /init.rc文件中

启动过程中执行的二进制文件在/system/bin/app_process

任何应用程序启动时,会从zygote进程fork出一个新的进程。并装载一些必要的class,invoke一些初始化方法。这其中包括:

ActivityThread ServiceThread ApplicationPackageManager

等应用启动中必要的类,触发必要的方法,比如handleBinApplication,将此进程与之对应的应用绑定的初始化方法;同时会将zygote进程中的dalvik虚拟机实例复制一份,因此每个应用程序进程都有自己的dalvik虚拟机实例;会将已有java运行时加载到进程中;会注册一些Android核心类的jni方法到虚拟机中,支撑从c到java的启动过程。

#AIDL

1 创建一个接口,再里面定义方法

package com.exapple.taidl;
interface ICalcAIDL{
    int add(int x, int y);
    int min(int x, int y);
}

build一下gen目录下会生成ICalcAIDL.java文件

新建一个Service

将服务端的aidl文件完整的复制过来,包名一定要一致。

服务端
private final ICalcAIDL.Stub mBinder = new ICalcAIDL.Stub()  
    {  
  
        @Override  
        public int add(int x, int y) throws RemoteException  
        {  
            return x + y;  
        }  
  
        @Override  
        public int min(int x, int y) throws RemoteException  
        {  
            return x - y;  
        }  
  
    };  


ICalcAILD.Stub来执行的,让我们来看看Stub这个类的声明:

public static abstract class Stub extends android.os.Binder implements com.zhy.calc.aidl.ICalcAIDL  

楚的看到这个类是Binder的子类,是不是符合我们文章开通所说的服务端其实是一个Binder类的实例 接下来看它的onTransact()方法:

文章开头也说到服务端的Binder实例会根据客户端依靠Binder驱动发来的消息,执行onTransact方法,然后由其参数决定执行服务端的代码。 可以看到onTransact有四个参数 codedatareplayflags


code 是一个整形的唯一标识,用于区分执行哪个方法,客户端会传递此参数,告诉服务端执行哪个方法
data客户端传递过来的参数
replay服务器返回回去的值
flags标明是否有返回值,0为有(双向),1为没有(单向)


客户端主要通过ServiceConnected与服务端连接

private ServiceConnection mServiceConn = new ServiceConnection()  
    {  
        @Override  
        public void onServiceDisconnected(ComponentName name)  
        {  
            Log.e("client", "onServiceDisconnected");  
            mCalcAidl = null;  
        }  
  
        @Override  
        public void onServiceConnected(ComponentName name, IBinder service)  
        {  
            Log.e("client", "onServiceConnected");  
            mCalcAidl = ICalcAIDL.Stub.asInterface(service);  
        }  
    };  
    
    如果你比较敏锐,应该会猜到这个onServiceConnected中的IBinder实例,其实就是我们文章开通所说的Binder驱动,也是一个Binder实例 在ICalcAIDL.Stub.asInterface中最终调用了:

return new com.zhy.calc.aidl.ICalcAIDL.Stub.Proxy(obj); 

这个Proxy实例传入了我们的Binder驱动,并且封装了我们调用服务端的代码,文章开头说,客户端会通过Binder驱动的transact()方法调用服务端代码

Service的作用其实就是为我们创建Binder驱动,即服务端与客户端连接的桥梁。

Binder机制

首先Binder是Android系统进程间通信IPC方式之一。

Binder使用Client--Service通信方式。Binder框架定义了四个角色:Service,Client,ServiceManager以及Binder驱动。其中Service,Client,ServiceManager运行于用户空间,驱动运行于内核空间。Binder驱动程序提供设备文件/dev/binder与用户空间交互,Client、Service和ServiceManager通过open和ioctl文件操作函数与Binder驱动程序进行通信。

Server创建了Binder实体,为其取一个字符形式,可读易记得名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给ServiceManager,通知ServiceManager注册一个名字为XX的Binder,它位于Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体结点以及注册一个名字为XX的Binder,它位于Service中。驱动为这个穿过进程边界的Binder创建位于内核中的实体结点以及ServiceManager对实体的引用,将名字以及新建的引用打包给ServiceManager。ServiceManager收数据包后,从中取出名字和引用填入一张查找表中。但是一个Server若向ServiceManager注册自己的Binder就必须通过0这个引用和ServiceManager的Binder通信。Service向ServiceManager注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。Clent也利用保留的0号引用向ServiceManager请求访问某个Binder:我申请名字叫XX的Binder的引用。ServiceManager收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder引用,将该引用作为回复发送给发起请求的Client。

请求访问某个Binder:我申请名字叫XX的Binder的引用。ServiceManager收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder引用,将该引用作为回复发送给发起请求的Client。

为什么Binder只进行了一次数据拷贝?

Linux内核实际上没有从一个用户空间到另一个用户空间直接拷贝的函数,需要先用copy_from_user()拷贝到内核空间,再用copy_to_user()拷贝到另一个用户空间。为了实现用户空间到用户空间的拷贝,mmap()分配的内存除了映射进了接收方进程里,还映射进了内核空间。所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,这就是Binder只需一次拷贝的‘秘密’。

最底层的是Android的ashmen(Anonymous shared memory)机制,它负责辅助实现内存的分配,以及跨进程所需要的内存共享。AIDL(android interface definition language)对Binder的使用进行了封装,可以让开发者方便的进行方法的远程调用,后面会详细介绍。Intent是最高一层的抽象,方便开发者进行常用的跨进程调用。

从英文字面上意思看,Binder具有粘结剂的意思那么它是把什么东西粘接在一起呢?在Android系统的Binder机制中,由一系统组件组成,分别是Client、Server、Service Manager和Binder驱动,其中Client、Server、Service Manager运行在用户空间,Binder驱动程序运行内核空间。Binder就是一种把这四个组件粘合在一起的粘连剂了,其中,核心组件便是Binder驱动程序了,ServiceManager提供了辅助管理的功能,Client和Server正是Binder驱动和ServiceManager提供的基础设施上,进行Client-Server之间的通信。

Client、Server和ServiceManager实现在用户空间中,Binder驱动实现在内核空间中 Binder驱动程序和ServiceManager在Android平台中已经实现,开发者只需要在用户空间实现自己的Client和Server Binder驱动程序提供设备文件/dev/binder与用户空间交互,Client、Server和ServiceManager通过open和ioctl文件操作函数与Binder驱动程序进行通信 Client和Server之间的进程间通信通过Binder驱动程序间接实现 ServiceManager是一个守护进程,用来管理Server,并向Client提供查询Server接口的能力

Client、Server和ServiceManager实现在用户空间中,Binder驱动实现在内核空间中 Binder驱动程序和ServiceManager在Android平台中已经实现,开发者只需要在用户空间实现自己的Client和Server Binder驱动程序提供设备文件/dev/binder与用户空间交互,Client、Server和ServiceManager通过open和ioctl文件操作函数与Binder驱动程序进行通信 Client和Server之间的进程间通信通过Binder驱动程序间接实现 ServiceManager是一个守护进程,用来管理Server,并向Client提供查询Server接口的能力

服务器端:一个Binder服务器就是一个Binder类的对象。当创建一个Binder对象后,内部就会开启一个线程,这个线程用户接收binder驱动发送的消息,收到消息后,会执行相关的服务代码。

Binder驱动:当服务端成功创建一个Binder对象后,Binder驱动也会相应创建一个mRemote对象,该对象的类型也是Binder类,客户就可以借助这个mRemote对象来访问远程服务。

客户端:客户端要想访问Binder的远程服务,就必须获取远程服务的Binder对象在binder驱动层对应的binder驱动层对应的mRemote引用。当获取到mRemote对象的引用后,就可以调用相应Binde对象的服务了。

在这里我们可以看到,客户是通过Binder驱动来调用服务端的相关服务。首先,在服务端创建一个Binder对象,接着客户端通过获取Binder驱动中Binder对象的引用来调用服务端的服务。在Binder机制中正是借着Binder驱动将不同进程间的组件bind(粘连)在一起,实现通信。

mmap将一个文件或者其他对象映射进内存。文件被映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会凋零。munmap执行相反的操作,删除特定地址区域的对象映射。

当使用mmap映射文件到进程后,就可以直接操作这段虚拟地址进行文件的读写等操作,不必再调用read,write等系统调用。但需注意,直接对该段内存写时不会写入超过当前文件大小的内容。

使用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次内存数据:一次从输入文件到共享内存区,另一次从共享内存到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域,而是保持共享区域,直到通信完成为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除内存映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

aidl主要就帮助了我们完成了包装数据和解包的过程,并调用了transact过程,而用来传递的数据包我们就称为parcel

用aidl定义需要被调用方法接口 实现这些方法 调用这些方法

Zygote和System进程的启动过程

init脚本的启动

+------------+    +-------+   +-----------+
|Linux Kernel+--> |init.rc+-> |app_process|
+------------+    +-------+   +-----------+
               create and public          
                 server socket

linux内核加载完成后,运行init.rc脚本

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server socket zygote stream 666

/system/bin/app_process Zygote服务启动的进程名 --start-system-server 表明Zygote启动完成之后,要启动System进程。 socket zygote stream 666 在Zygote启动时,创建一个权限为666的socket。此socket用来请求Zygote创建新进程。socket的fd保存在名称为“ANDROID_SOCKET_zygote”的环境变量中。

MVC MVP MVVM的区别

MVC 可以分为三个部分

1 视图(View)用户界面

2 控制器(Controller)业务逻辑

3 模型(Model)数据保存

各部分之间通信方式如下:

1 View传送指令到Controller

2 Controller完成业务逻辑后,要求Model改变状态

3 Model将新的数据发送到View,用户得到反馈

Tips:所有的通信都是单向的。

互动模式,接受用户指令时,MVC可以分为两种方式。一种是通过View接受指令,传递给Controller。

另一种是直接通过Controller接受指令

#MVP MVP模式将Controller该名为Presenter,同时改变了通信方向。

1 各部分之间的通信都是双向的。

2 View和Model之间不发生联系,都是通过Presenter传递

3 View非常薄,不部署任何业务逻辑,称为“被动视图”既没有任何主动性,而Presenter非常厚,所有逻辑都部署在那里。

#MVVM

MVVM模式将Presenter改名为ViewModel,基本上与MVP模式完全一致。

唯一的区别是,它采用双向绑定:View的变动,自动反映在ViewModel,反之亦然。

为什么需要MVP

1 尽量简单,大部分Android应用使用View-Model结构,程序员现在更多的是和复杂的view打交道而不是解决业务逻辑。当你在应用中只使用Model-View时,到最后,你会发现“所有的事物都被连接到一起”。复杂的任务被分成细小的任务,并且很容易解决。越小的东西,bug越少,越容易debug,更好测试。在MVP模式下的View层将会变得简单,所以即便是他请求数据的时候也不需要回调函数。View逻辑变成十分直接。

后台任务 当你编写一个Activity、Fragment、自定义View的时候,你会把所有的和后台任务相关的方法写在一个静态类或者外部类中。这样,你的Task不再和Activity联系在一起,这既不会导致内存泄漏,也不依赖于Activity的重建。

优缺点

优点:

1 降低耦合度,实现了Model和View真正的完全分离,可以修改View而不影响Modle

2 模块职责划分明显,层次清晰

3 隐藏数据

4 Presenter可以复用,一个Presenter可以用于多个View,而不需要更改Presenter的逻辑(当然是在View的改动不影响业务逻辑的前提下)

5 利于测试驱动开发,在使用MVP的项目中Presenter对View是通过接口进行,在对Presenter进行不依赖UI环境的单元测试的时候,可以通过Mock一个View对象,这个对象只需要实现了View的接口即可,然后依赖注入到Presenter中,单元测试的时候就可以完整的测试Presenter应用逻辑的正确性。

6 View可以进行组件化。在MVP中,View不依赖Model,这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到 对业务完全无知,它只需要一系列接口提供给上层操作。这样就可以做到高度可复用的View组件。

7 代码灵活性

缺点:

1 Presenter中除了应用逻辑以外,还有大量的View-->Model-->的手动同步逻辑,造成Presenter比较笨重,维护起来比较困难。

2 由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。

3 如果Presenter过多的渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了。

4 额外的代码复杂度及学习成本。

在MVP模式里通常包含4个要素:

1 View负责绘制UI元素、与用户进行交互(Android中体现为Activity);

2 View interface需要View实现的接口,View通过View interface与Presenter进行交互,降低耦合,方便进行单元测试;

3 Model 负责存储、检索、操作数据

4 Presenter 作为View与Model交互的中间纽带,处理与用户交互的负责逻辑。

#Android开机过程

BootLoder引导,然后加载Linux内核

0号进程init启动。加载init.rc配置文件,配置文件有个命令启动了zygote进程

zygote开始fork出SystemServer进程

SystemServer加载各种jni库,然后init1,init2方法,init2方法中开启了新线程ServerThread

在SystemServer中会创建一个socket客户端,后续AMS会通过此客户端和zygote通信

ServerThread的run方法中开启了AMS,还孵化新进程ServiceManager,加载注册了一溜的服务,最后一句话进入loop 死循环

AMS,还孵化新进程ServiceManager,加载注册了一溜的服务,最后一句话进入loop 死循环 run方法的SystemReady调用resumeTopActivityLocked打开锁屏界面

0 Retrofit2 源码分析

开发Android App 肯定会使用Http请求与服务器通信,上传或下载数据等。目前开源的Http请求工具也有很多,比如Google开发的Volley,Square开源的OKHttp或者Retrofit等。

我觉得Retrofit无疑是这几个当中最好用的一个,设计这个库的思路很特别而且巧妙,Retrofit的代码很少。

分析: 0 Retrofit是什么, 1 Retrofit怎么用, 2 Retrofit的原理是什么, 3 心得和看法。

#Retrofit是什么

是基于OKHttp的RESTFUL Api请求工具,从功能上来说和Google的Volley功能上很相似,但是使用上不同。

Volley使用上更加原始而且符合使用者的直觉,当App要发送一个Http请求时,你需要先创建一个Request对象,指定这个Request用的是GET还是POST或者其他方法,一个api地址,一个处理response的回调,如果是一个POST请求,那么你还需要给这个Request对象设置一个body,有时候你还需要自定义添加Header什么的,然后将这个Request对象添加到RequestqQueue中,接下去检查Cache以及发送Http请求的事情,Volley会帮我们处理。如果一个app中不同的api请求很多,这样代码就会很难看。

而Retrofit可以让你简单到调用一个java方法的方式去请求一个api,这样app中的代码就会很简洁方便阅读

1 Retrofit怎么用

首先需要创建一个Retrofit对象,并且制定api的域名

public static final String API_URL = "https://zhuanlan.zhihu.com";

Create a very simple REST adapter which points the Zhuanlan API.
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl(API_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .build();

其次,要根据api新建一个java接口,用java注解来描述这个api

public interface ZhuanLanApi {
    @GET("/api/columns/{user} ")
    Call<ZhuanLanAuthor> getAuthor(@Path("user") String user)
}

再用这个Retrofit对象创建一个ZhuanLanApi对象:

ZhuanLanApi api = retrofit.create(ZhuanLanApi.class);

Call<ZhuanLanAuthor> call = api.getAuthor("qinchao");

这样就表示你要请求的api是https://zhuanlan.zhihu.com/api/columns/qinchao

最后你就可以用这个call对象获得数据了,enqueue方法是异步发送Http请求的,如果你想用同步的方式发送可以使用execute方法,call对象还提供cancel、isCance等方法获取这个Http请求的状态

// 请求数据,并且处理response
call.enqueue(new Callback<ZhuanLanAuthor>() {
    @Override
    public void onResponse(Response<ZhuanLanAuthor> author) {
        System.out.println("name: " + author.getName());
    }
    @Override
    public void onFailure(Throwable t) {
    }
});


Retrofit只要创建一个接口来描述Http请求,然后可以让我们可以像调用Java方法一样请求一个Api

2 Retrofit的原理

从上面的Retrofit的使用来看,Retrofit就是充当了一个适配器(Adapter)的角色:将一个java接口翻译成一个Http请求,然后用OKHttp去发送这个请求

Volley描述一个Http请求是需要创建一个Request对象,而执行这个请求呢?就是把这个请求对象放到一个队列中,在网络请求中用HttpUrlConnection去请求

Retrofit是怎么做的?

使用java的动态代理

动态代理

我刚开始看Retrofit的代码,我对下面这句代码感到很困惑:

ZhuanLanApi api = retrofit.create(ZhuanLanApi.class);

我给Retrofit对象传了一个ZhuanLanApi接口的Class对象,怎么又返回一个ZhuanLanApi对象呢?进入create方法一看,没几行代码,但是我觉得这几行代码就是Retrofit的精妙的地方:

/** Create an implementation of the API defined by the {@code service} interface. */
public <T> T create(final Class<T> service) {
  Utils.validateServiceInterface(service);
  if (validateEagerly) {
     eagerlyValidateMethods(service);
  }
  return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
    new InvocationHandler() {
      private final Platform platform = Platform.get();

      @Override public Object invoke(Object proxy, Method method, Object... args)
          throws Throwable {
        // If the method is a method from Object then defer to normal invocation.
        if (method.getDeclaringClass() == Object.class) {
          return method.invoke(this, args);
        }
        if (platform.isDefaultMethod(method)) {
          return platform.invokeDefaultMethod(method, service, proxy, args);
        }
        ServiceMethod serviceMethod = loadServiceMethod(method);
        OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
        return serviceMethod.callAdapter.adapt(okHttpCall);
      }
    });

create方法就是返回了一个 Proxy.newProxyInstance动态代理对象。

动态代理是什么东西

看Retrofit代码之前我知道java动态代理是一个很重要的东西,比如在Spring框架里大量的用到,但是它有什么用呢?

java动态代理就是给程序员一种可能:当你要调用某个Class的方法前或后,插入你想要执行的代码

比如你要执行某个操作前,你必须要判断这个用户是否登录,或者你在付款前,你需要判断这个人的账户中存在这么多钱。这么简单的一句话,我相信可以把一个不懂技术的人也讲明白Java动态代理是什么东西了。

为什么要使用动态代理

Call<ZhuanLanAuthor> call = api.getAuthor("qinchao");

上面api对象其实是一个动态代理对象,并不是一个真正的ZhuanLanApi接口的implements产生的对象,当api对象调用getAuthor方法时会被动态代理拦截,然后调用Proxy.newProxyInstance方法中的InvocationHandler对象,它的invoke方法会传入3个参数:

Object proxy: 代理对象,不关心这个

Method method:调用的方法,就是getAuthor方法

Object... args:方法的参数,就是"qinchao"

而Retrofit关心的就是method和它的参数args,接下去Retrofit就会用java反射获取到getAuthor方法的注解信息,配合args参数,创建一个ServiceMethod对象

ServiceMethod就像是一个中央处理器,传入Retrofit对象和Method对象,调用各个接口和解析器,最终生成一个Request,包含api的域名、path、http请求方法、请求头、是否有body、是否是multipart等等。最后返回一个call对象,Retrofit2中Call接口的默认实现是OkHttpCall,它默认使用OkHttp3作为底层请求client。

使用java动态代理的目的就要拦截被调用的java方法,然后解析这个java方法的注解,最后生成Request由OkHttp发送

3 Retrofit的源码分析

想要弄清楚Retrofit的细节,先来看一下Retrofit源码的组成:

1 一个retrofit2.http包,里面全部是定义HTTP请求的java注解,比如GET、POST、PUT、DELETE、Headers、Path、Query等等

2 余下的retrofit2包中几个类和接口就是全部retrofit的代码了,代码真的很少,很简单,因为retrofit把网络请求这部分功能全部交给了OkHttp了

Retrofit接口

Retrofit的设计非常插件化而且轻量级,真的是非常高内聚而且低耦合,这个和它的接口设计有关。Retrofit中定义了4个接口:

Callback<T>

这个接口就是Retrofit请求数据返回的接口,只有两个方法

void onResponse(Response<T> response);

void onFailure(Throwable t);

Converter<F, T>

这个接口主要的作用就是将Http返回解析成java对象,主要有xml、gson、protobuf等等,你可以在创建Retrofit对象时添加你需要使用的Converter实现

Call<T>

这个接口主要的作用就是发送一个HTTP请求,Retrofit默认的实现是OkHttpCall<T>,你可以根据实际情况实现你自己的Call类,这个设计和Volley的HttpStack接口设计的思想非常相似,子类可以实现基于HttpClient或HttpUrlConnetction的Http请求工具,这种设计非常的插件化,而且灵活

CallAdapter<T>

上面说到过,CallAdapter中属性只有responseType一个,还有一个<R> T adapt(Call<R> call)方法,这个接口的实现类也只有一个,DefaultCallAdapter。这个方法的主要作用就是将Call对象转换成另一个对象,可能是为了支持RxJava才设计这个类的吧

Retrofit的运行过程

上面讲到ZhuanLanApi api = retrofit.create(ZhuanLanApi.class);代码返回了一个动态代理对象,而执行Call<ZhuanLanAuthor> call = api.getAuthor("qinchao");代码时返回了一个OkHttpCall对象,拿到这个Call对象才能执行HTTP请求

上面api对象其实是一个动态代理对象,并不是一个真正的ZhuanLanApi接口的implements产生的对象,当api对象调用getAuthor方法时会被动态拦截,然后调用Proxy.newProxyInstance方法中的InvocationHandler对象, 创建一个ServiceMethod对象

ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);

创建ServiceMethod

刚才说到,ServiceMethod就像是一个中央处理器,具体来看一下创建这个ServiceMethod的过程是怎么样的

第一步,获取到上面说到的3个接口对象:

callAdapter = createCallAdapter();
responseType = callAdapter.responseType();
responseConverter = createResponseConverter();

第二步,解析Method的注解,主要就是获取Http请求的方法,比如是GET还是POST还是其他形式,如果没有,程序就会报错,还会做一系列的检查,比如如果在方法上注解了@Multipart,但是Http请求方法是GET,同样也会报错。因此,在注解Java方法是需要严谨

for (Annotation annotation : methodAnnotations) {
    parseMethodAnnotation(annotation);
}

if (httpMethod == null) {
   throw methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.).");
}

第三步,比如上面api中带有一个参数{user},这是一个占位符,而真实的参数值在Java方法中传入,那么Retrofit会使用一个ParameterHandler来进行替换:

int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];

最后,ServiceMethod会做其他的检查,比如用了@FormUrlEncoded注解,那么方法参数中必须至少有一个

执行Http请求

之前讲到,OkHttpCall是实现了Call接口的,并且是真正调用OkHttp3发送Http请求的类。OkHttp3发送一个Http请求需要一个Request对象,而这个Request对象就是从ServiceMethod的toRequest返回的

总的来说,OkHttpCall就是调用ServiceMethod获得一个可以执行的Request对象,然后等到Http请求返回后,再将response body传入ServiceMethod中,ServiceMethod就可以调用Converter接口将response body转成一个Java对象

你可能会觉得我只要发送一个HTTP请求,你要做这么多事情不会很“慢”吗?不会很浪费性能吗?

我觉得,首先现在手机处理器主频非常高了,解析这个接口可能就花1ms可能更少的时间(我没有测试过),面对一个HTTP本来就需要几百ms,甚至几千ms来说不值得一提;而且Retrofit会对解析过的请求进行缓存,就在Map<Method, ServiceMethod> serviceMethodCache = new LinkedHashMap<>();这个对象中

如何在Retrofit中使用RxJava

由于Retrofit设计的扩展性非常强,你只需要添加一个CallAdapter就可以了

Retrofit retrofit = new Retrofit.Builder()
  .baseUrl("https://api.github.com")
  .addConverterFactory(ProtoConverterFactory.create())
  .addConverterFactory(GsonConverterFactory.create())
  .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
  .build();

上面代码创建了一个Retrofit对象,支持Proto和Gson两种数据格式,并且还支持RxJava

4 最后

Retrofit非常巧妙的用注解来描述一个HTTP请求,将一个Http请求抽象成一个java接口,然后用了java动态代理的方式,动态的将这个接口的注解“翻译”成一个HTTP请求,最后执行这个HTTP请求。

Retrofit的功能非常多的依赖Java反射,代码中其实还有很多细节,比如异常的捕获、抛出和处理,大量的Factory设计模式(为什么要这么多使用Factory模式?)

Retrofit中接口设计的恰到好处,在你创建Retrofit对象时,让你有更多更灵活的方式去处理你的需求,比如使用不同的Converter、使用不同的CallAdapter,这也就提供了你使用RxJava来调用Retrofit的可能。

Glide内存缓存:默认是LruResourceCache

EventBus 是一款针对Android优化的发布,订阅事件总线,主要功能是替代Intent,Handler,BroadCast在Fragment,Activity,Service,线程之间传递消息。简化了应用程序内各组件间、组件与后台线程间的通信。优点是开销小,代码更优雅。以及将发送者和接受者解耦。比如两个Fragment之间需要通过Listener通信,这些需求都可以通关EventBus实现。

EventBus作为一个消息总线,有三个主要的元素:

Event:事件。可以是任意类型的对象

Subscriber:事件订阅者,接收特定的事件

Publisher:事件发布者,用于通知 Subscriber 有事件发生。可以在任意线程任意位置发送事件,直接调用eventBus.post(Object) 方法,可以自己实例化 EventBus 对象,但一般使用默认的单例就好了:EventBus.getDefault(), 根据post函数参数的类型,会自动调用订阅相应类型事件的函数。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android知识梳理 的相关文章

  • EFCore——三种关系的配置(9)

    关系的配置之一对多 一 关系配置的套路二 一对多1 创建实体2 实体配置3 迁移4 控制台进行数据操作5 关系依赖添加 二 一对一1 实体属性2 配置关系 三 多对多 一 关系配置的套路 EFCore中配置的套路 HasXXX WithXX
  • NetCore缓存——内存缓存(3)

    内存缓存 一 缓存数据类型二 缓存特性三 用法 一 缓存数据类型 缓存放到应用程序的内存中 内存缓存中保存的是一系列的键值对 就像Dictionary类型一样 二 缓存特性 内存缓存保存在当前运行的网站程序的内存中是和进程相关的 因为在we
  • 流程审批系统设计思路及实现方法

    背景 流程审批系统是一个很常见的系统 xff0c 包括我们在日常权限申请 xff0c 订单状态流转等很多场景都会接触 其核心的点有两个 xff1a 1 状态流转 2 流程驱动 我们以我们常见的流程审批为例 xff0c 比如我想向上街申请一台
  • 【问题】Ubuntu20.04桌面某些图标不显示

    问题 x1f631 xff1a 在桌面新建了test md xff0c 保存后关闭 桌面没有对应文件的图标 分析 x1f42f xff1a gnome shell问题 解决方案 x1f489 xff1a 重启gnome shell 按下al
  • 【python】连接远程服务器并传输文件,执行命令,传回文件

    使用Paramiko完成连接远程服务器并传输文件 执行命令 传回文件的功能 1 安装 pip install paramiko 2 config json nbsp nbsp file path home test test nbsp nb
  • centos7 安装GNOME 使用vnc连接

    1 xff1a 查看当前系统运行级别 命令 xff1a runlevel root span class hljs decorator 64 wst runlevel span N span class hljs number 3 span
  • 升级AS gradle错误

    错误信息 Direct local aar file dependencies are not supported when building an AAR 最近升级了AS和gradle 在编译时总是提示 gt Direct local a
  • 【项目实战】Spring体系结构与框架图

    一 Spring 体系结构 Spring 有可能成为所有企业应用程序的一站式服务点 xff0c 然而 xff0c Spring 是模块化的 xff0c 允许你挑选和选择适用于你的模块 xff0c 不必要把剩余部分也引入 下面的部分对在 Sp
  • sizeof的使用方法!!!

    sizeof xff08 int xff09 等的大小依赖于操作系统的位数 xff0c 如果是在 16 位 DOS 环境下 xff0c 用 TC 编译 xff0c 则为 2 xff0c 如果在 windows 环境下 xff0c 用 VC
  • sql中like的所有用法

    在sql结构化查询语言中 xff0c like语句有着至关重要的作用 like语句的语法格式是 xff1a select from 表名 where 字段名 like 对应值 xff08 子串 xff09 xff0c 它主要是针对字符型字段
  • FTP文件服务器拉取不存在的文件后出现连接不上的问题

    公司使用ftp作为各个业态文件交互中转站 xff0c 此FTP功能由我负责开发维护 开发使用的语言是java xff0c 初始化连接代码如下 xff1a span class token keyword public span FTPSCl
  • kali镜像下载官网地址

    http www kali org downloads
  • 如何powershell用7z命令批量压缩文件

    eidt by lfq date 20220915 use to 压缩 如何使用powershell执行7z命令 1 把压缩包里的两份文件放到C盘C Windows System32下 2 win 43 r输入cmd执行7z a file
  • Ubuntu20.04 安装 Google Chrome浏览器

    一 在 Ubuntu 上安装 Google Chrome 1 下载 Google Chrome 使用Ctrl 43 Alt 43 T快捷键或者点击终端图标 xff0c 打开你的终端 安装软件时最好是先更新一下 xff01 sudo apt
  • hexo博客5:更新部署&域名配置

    hexo博客5 xff1a 更新部署 amp 域名配置 一 Hexo更新二 部署脚本三 自定义域名四 参考 一 Hexo更新 管理员模式进入cmd hexo clean hexo generate hexo deploy 二 部署脚本 也可
  • Windows远程连接centos7图形化界面,安装xrdp

    Windows远程连接centos7图形化界面 xff0c 安装xrdp 写在最前面准备工作查看ubuntu系统的版本信息和gcc版本尝试进入图形化界面更新yum 下载安装图形化界面查询本地是否有Server with GUI group安
  • 华为机试—围棋吃子(下围棋)判决(高级题160分:深度优先遍历)(图文吐血整理)

    题目 xff1a 围棋中 xff0c 一个棋子在棋盘上 xff0c 与它直接紧邻的空点是这个棋子的 气 xff0c 棋子直接紧邻的点上 xff0c 如果有同色妻子存在 xff0c 则它们便相互组成一个不可分割的整体 xff0c 它们的 气
  • ConstraintLayout 通过setVerticalBias 实现动态设置控制位置

    使用场景 xff1a 设置布局的时候 xff0c 想通过ConstraintLayout实现滑块与文字同步移动 思路 xff1a ConstraintLayout在xml布局中有layout constraintvertical bias设
  • gdebi来安装依赖关系

    gdebi是一个用于安装你自己手动下载的包的GUI程序 GDebi也可以命令行模式运行 xff08 sudo gdebipackage deb xff09 xff0c 其功能和GUI模式下完全一样 安装 xff1a apt get inst
  • &和&&的区别?

    答 xff1a amp 运算符有两种用法 xff1a 1 按位与 xff1b 2 逻辑与 amp amp 运算符是短路与运算 逻辑与跟短路与的差别是非常巨大的 xff0c 虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是t

随机推荐

  • Spring的五种依赖注入方式

    平常的java开发中 xff0c 程序员在某个类中需要依赖其它类的方法 xff0c 则通常是new一个依赖类再调用类实例的方法 xff0c 这种开发存在的问题是new的类实例不好统一管理 xff0c spring提出了依赖注入的思想 xff
  • Unity VR游戏开发干货教程:优化VR体验

    简介 对于VR应用来说 xff0c 如果想要让用户获得好的用户体验 xff0c 特别是免除恶心眩晕的困扰 xff0c 在VR开发中进行优化是必不可少的 xff0c 惟其如此才能达到我们期望的游戏运行帧速 和其它平台上的开发不同 xff0c
  • 使用lombok编译时报错:程序包org.slf4j不存在

    原文链接 xff1a http www jylt cc detail id 61 67987702f9160c26a14d3a421f43dce1 在使用lombok插件打印日志时 xff0c 编译时候报错 xff0c 只需做如下修改即可
  • 企业对C/C++程序员的技能要求

    一个人应该具备对事物的思考能力 xff0c 否则容易被忽悠 对大部分未入门或刚入门的菜鸟来说 xff0c 很难搞明白C语言能做什么和C程序员在做什么这两个问题 如果你打算种菜 xff0c 必须先了解行情 xff08 包括销量和价钱 xff0
  • 如何让 Shell 提示符更酷炫

    使用远程终端时 xff0c 默认的命令行提示符格式已经能满足大部分用户需求了 xff0c 但有时我们希望提示符看起来更直观 优雅 酷炫 美观 xff0c 可以从中直接得到我们想要的信息 xff0c 而且清晰分明 本文就详细讲解一下如何让 S
  • 写给大侄女

    老姑从你上高中开始 xff0c 就想写点东西给大侄女看 xff0c 不过老姑理科出身 xff0c 文笔比较差 不知道该不该提你在学校看手机的事情 xff0c 老姑没有责备你的意思 xff0c 只是和你探讨一下 xff0c 毕竟谁没有年轻的时
  • centos安装lspci工具

    背景 由于centos6 3迷你安装版上没有带lspci工具 在定制内核时 无法用此工具查询硬件相关信息 具体步骤如下 1 下载 pci包 xff1a http www kernel org pub software utils pciut
  • 软件性能测试方法论

    软件性能测试过程详解与案例分析 xff08 段念 编著 xff09 学习笔记三 1 SEI负载测试计划过程 SEI load Testing Planning Process是一个关注于负载测试计划的方法 xff0c 其目标是产生 清晰 易
  • Android Studio使用Kotlin时Execution failed for task ':app.compileDebugKotlin'.问题

    最近在接触kotlin编写android xff0c 有些坑必须得踩 kotlin插件依赖添加成功以后 xff0c 突然爆一个Execution failed for task 39 app compileDebugKotlin 39 go
  • HTTP Get,Post请求详解

    请求类型 三种最常见的请求类型是 xff1a GET xff0c POST 和 HEAD GET xff1a 获取一个文档 大部分被传输到浏览器的html xff0c images xff0c js xff0c css 都是通过GET方法发
  • Linux查看端口使用状态、关闭端口方法

    前提 xff1a 首先你必须知道 xff0c 端口不是独立存在的 xff0c 它是依附于进程的 某个进程开启 xff0c 那么它对应的端口就开启了 xff0c 进程关闭 xff0c 则该端口也就关闭了 下次若某个进程再次开启 xff0c 则
  • 查找列表中某个值的位置(python)

    p 61 list index value list为列表的名字 value为查找的值 p为value在list的位置 以下内容引自 xff1a http www linuxidc com Linux 2012 01 51638 htm P
  • python 等待一定时间后继续执行其后的程序

    简单示例 xff1a import time print 39 11 39 time sleep 10 print 39 22 39 先打印11 xff0c 等待10秒后 xff0c 打印22
  • Linux下用于查看系统当前登录用户信息的4种方法

    作为系统管理员 xff0c 你可能经常会 xff08 在某个时候 xff09 需要查看系统中有哪些用户正在活动 有些时候 xff0c 你甚至需要知道他 xff08 她 xff09 们正在做什么 本文为我们总结了4种查看系统用户信息 xff0
  • TCP:三次握手,URG、ACK、PSH、RST、SYN、FIN 含义

    TCP SYN ACK FIN RST PSH URG简析 三次握手Three way Handshake 一个虚拟连接的建立是通过三次握手来实现的 1 B gt SYN gt A 假如服务器A和客户机B通讯 当A要和B通信时 xff0c
  • [转载]一次 JMeter 脚本请求错误 HTTP Status 415 的解决笔记

    录制好脚本以后 xff0c 使用 JMeter 打开 xff0c 直接运行测试 xff0c 发现有个 Ajax 提交表单的时候出错了 服务器返回信息如下 xff1a HTTP Status 415 type Status report me
  • python list转换字符串报错TypeError: sequence item 0: expected str instance, int found

    今天敲小例子 xff0c 报了错TypeError sequence item 0 expected str instance int found 小例子 xff1a list1 61 1 39 two 39 39 three 39 4 p
  • 初尝WSL(Windows Subsystem for Linux)

    微软的WSL发布也有一段时间了 xff0c 一直未尝试过 windows兼容linux子系统 xff0c 再联系最近微软windows部门整改 xff0c 不由感叹 由于工作是在windows环境下开发服务器程序 xff0c 对主流服务器操
  • SPI protocol 驱动编写 Part 1

    Linux 中 SPI 系统概览 Contents Part 1 Linux中 SPI子系统概览 Part 2 SPI message基础 Part 3 异步写 Overview SPI框架的内核文档是个好的开始 在你的内核源码中 Docu
  • Android知识梳理

    Android中5种布局 xff1a FrameLayout xff0c LinearLayout RelativeLayout TableLayout全部继承ViewGroup Activity和Fragment的关系 xff1a onA