Android 性能优化之布局优化

在Android应用开发中,常用的布局方式主要有LinearLayout、RelativeLayout、FrameLayout等,通过这些布局可以实现各种各样的界面。我们需要知道如何高效地使用这些布局方式来组织UI控件,布局的好坏影响到绘制的时间,本节将通过减少Layout层级减少测量绘制时间提高复用性三个方面来优化布局,优化的目的就是减少层级,让布局扁平化,以提高绘制的时间,提高布局的复用性节省开发和维护成本。<!--more-->

减少层级

层级越少,测试和绘制的时间就越短,通常减少层级有以下两个常用方案: 合理使用 RelativeLayout 和 LinearLayout。 合理使用Merge。

(1)RelativeLayout与LinearLayout

使用 RelativeLayout 减少层级:

!

通过Hierarchy View来查看下层级情况:

!

可以看到一共有7级,使用RelativeLayout进行优化,达到相同的布局效果,并且RelativeLayout允许子元素指定它们相对于其他元素或父元素的位置,有最大自由度的布局属性,而且布局层次最浅,占用内存最少。

!

这样就可以减少两个层级,用一个RelativeLayout就可以达到显示的效果,再使用Hierarchy View来查看层级,可以看到减少到5层。

但ReativeLayout也存在性能低的问题,原因是RelativeLayout会对子View做两次测量,在RelativeLayout中子View的排列方式是基于彼此的依赖关系,因为这个依赖关系可能和布局中View的顺序并不相同,在确定每个子View的位置时,需要先给所有子View做一次排序。如果在RelativeLayout中允许子View横向和纵向互相依赖,就需要横向、纵向分别进行一次排序测量。但如果在LinearLayout中有weight属性,也需要进行两次测量,因为没有更多的依赖关系,所以仍然会比RelativeLayout的效率高,在布局上RelativeLayout不如LinearLayout快

但是如果布局本身层次太深,还是推荐用RelativeLayout减少布局本身层次,相较于测量两次,虽然会增加一些计算时间,但在体验上影响不会特别大,如果优化掉两层仅仅是增加一次测量,还是非常值得的,布局层次深会增加内存消耗,甚至引起栈溢出等问题,即使耗点时间,也不能让应用不可用。

根据以上分析,可以总结出以下几点布局原则: 尽量使用RelativeLayout和LinearLayout。 在布局层级相同的情况下,使用LinearLayout。 用LinearLayout有时会使嵌套层级变多,应该使用RelativeLayout,使界面尽量扁平化。

注意 由于Android的碎片化程度很高,市面上的屏幕尺寸也是各式各样,使用RelativeLayout能使构建的布局适应性更强,构建出来的UI布局对多屏幕的适配效果更好,通过指定UI控件间的相对位置,使不同屏幕上布局的表现基本保持一致。当然,也不是所有情况下都得使用相对布局,根据具体情况选择和搭配使用其他布局方式来实现最优布局。

(2)Merge的使用

从名字上就可以看出,Merge就是合并的意思。使用它可以有效优化某些符合条件的多余的层级。使用Merge的场合主要有以下两处:

在自定义View中使用,父元素尽量是FrameLayout或者LinearLayout。 在Activity中整体布局,根元素需要是FrameLayout。

我们仍以前面的布局为例,在页面增加一个自定义控件TopBar:

其中TopBar的XML布局如下:

显示结果下图所示。这种布局在一些列表的Item中非常常见,而且列表中Item本身的层级比较深,因此优化显得更有意义。

我们使用HierarchyView查看增加TopBar后的布局层级,如图。可以看到,就是这么简单的一个布局,却把层级增加了两级,从图中很明显地看出TopBar后一层的LinearLayout是多余的,这时可以使用Merge把这一层消除。

使用Merge来优化布局,使用Merge标签替换LinearLayout后,原来的LinearLayout属性也没有用了,修改后的代码如下

运行后再使用Hierarchy View查看当前层级

这样就把多余的LinearLayout消除了,原理是在Android布局的源码中,如果是Merge标签,那么直接将其中的子元素添加到Merge标签Parent中,这样就保证了不会引入额外的层级。

注意 如果Merge代替的布局元素为LinearLayout,在自定义布局代码中将LinearLayout的属性添加到引用上,如垂直或水平布局、背景色等。

但Merge不是所有地方都可以任意使用,有以下几点要求:

Merge只能用在布局XML文件的根元素。

使用merge来加载一个布局时,必须指定一个ViewGroup作为其父元素,并且要设置加载的attachToRoot参数为true(参照inf late(int, ViewGroup, boolean))。

不能在ViewStub中使用Merge标签。原因就是ViewStub的inf late方法中根本没有attachToRoot的设置。

这一节讲了如何减少层级,那么在Android系统中,多少层才是合理的呢?当然是越少越好,但从Lint检查的配置上看,超过10层才会报警,实际上在开发时,随着产品设计的丰富和多样性,很容易超过10层,根据实际开发过程中超过15层就要重视并准备做优化,20层就必须修改了。在实在没有办法优化的情况下,需要把复杂的层级用自绘控件来实现,自绘控件中的图层层级再多,在布局上也只是一层,但这样也会带来过度绘制的问题,后面会讲。

注意 在Activiy的总布局中使用Merge,但又想设置整体的属性(布局方式或背景色),可以不使用setContentView方法加载Layout,而使用(id/content)将FrameLayout取出来,在代码中手动加载布局,但如果层级压力不大(小于10级),则没有必要,因为这样代码的维护性较差。

提高显示速度

我们在开发的过程中会碰到这样的场景或者显示逻辑:某个布局当中的子布局非常多,但并不是所有元素都同时显示出来,而是二选一或者N选一,打开这个界面根据不同的场景和属性显示不同的Layout。例如:一个页面对不同的用户(未登录、普通用户、会员)来说,显示的布局不同。或者,有些用户喜欢对不同的元素使用INVISIBLE或者GONE隐藏,通过设计元素的visable属性来控制,这样虽然达到了隐藏的目的,但效率非常低,原因是即使将元素隐藏,它们仍在布局中,仍会测试和解析这些布局。Android提供了ViewStub控件来解决这个场景。

ViewStub是一个轻量级的View,它是一个看不见的,并且不占布局位置,占用资源非常小的视图对象。可以为ViewStub指定一个布局,加载布局时,只有ViewStub会被初始化,然后当ViewStub被设置为可见时,或是调用了ViewStub.inf late()时,ViewStub所指向的布局会被加载和实例化,然后ViewStub的布局属性都会传给它指向的布局。这样,就可以使用ViewStub来设置是否显示某个布局。

下面的代码是两个ViewStub通过不同的初始化来加载两个不同的布局,以满足用户的需求。

在调用时,根据需求切换不同的Layout,这样可以提高页面初始化的速度,使用代码如下:

ViewStub显示有两种方式,上面代码使用的是inf ate方法,也可以直接使用ViewStub.setVisibiltity(View.Visible)方法。 使用ViewStub时需要注意以下几点:

  1. ViewStub只能加载一次,之后ViewStub对象会被置为空。换句话说,某个被ViewStub指定的布局被加载后,就不能再通过ViewStub来控制它了。所以它不适用于需要按需显示隐藏的情况。

  2. ViewStub只能用来加载一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中。如果想操作一个具体的View,还是使用visibility属性。

  3. VIewStub中不能嵌套Merge标签。

不过这些限制都无伤大雅,我们还是能够用ViewStub来做很多事情,ViewStub的主要使用场景如下: 在程序运行期间,某个布局在加载后,就不会有变化,除非销毁该页面再重新加载。 想要控制显示与隐藏的是一个布局文件,而非某个View。

注意 因为ViewStub只能Inf late一次之后会被置空无法继续使用ViewStub来控制布局。所以当需要在运行时不止一次显示和隐藏某个布局时,使用ViewStub是无法实现的。这时只能使用View的可见性来控制。

布局复用

我们在开发应用时还会碰到另一个常见的场景,就是一个相同的布局在很多页面(Activity或Fragment)会用到,如果给这些页面的布局文件都统一加上相同的布局代码,维护起来就很麻烦,可读性也差,一旦需要修改,很容易有漏掉的地方,Android的布局复用可以通过<include>标签来实现,就像提取代码公用部分一样,在编写Android布局文件时,也可以将相同的部分提取出来,在使用时,用<include>添加进去。

例如,在大部分应用中,基本上所有的应用都会带有头部栏(TopBar),主要是显示标题和返回键功能,这样只需要维护一份代码,就可以修改所有的显示效果。

提示 类似于TopBar的这类常用控件,包括菜单,可以把具体实现抽象到页面的基类(BaseActivity)中,这样布局和具体的实现都收归到一个地方,方便维护。 提高布局效率的方法总体来说就是减少层级,提高绘制速度和布局复用。影响布局效率主要有以下几点:

布局的层级越少,加载速度越快。 减少同一层级控件的数量,加载速度会变快。 一个控件的属性越少,解析越快。

根据本节的分析,对优化的总结如下: 尽量多使用RelativeLayout或LinearLayout,不要使用绝对布局AbsoluteLayout。 将可复用的组件抽取出来并通过< include />标签使用。 使用< ViewStub />标签加载一些不常用的布局。 使用< merge />标签减少布局的嵌套层次。 尽可能少用wrap_content, wrap_content会增加布局measure时的计算成本,已知宽高为固定值时,不用wrap_content。 删除控件中的无用属性。

避免过度绘制

过度绘制(Overdraw)是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的UI结构(如带背景的TextView)中,如果不可见的UI也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费多余的CPU以及GPU资源。

当设计上追求更华丽的视觉效果时,我们很容易陷入采用复杂的多层次重叠视图来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳性能,必须尽量减少Overdraw情况发生。

我们一般在XML布局和自定义控件中绘制,因此可以看出导致过度绘制的主要原因是: XML布局->控件有重叠且都有设置背景 View自绘-> View.OnDraw里面同一个区域被绘制多次

如何查看是否有过度绘制:

在手机的“设置”→“开发者选项”中打开“显示GPU过度重绘”开关(注:对未默认开启硬件加速的界面需要同时打开“强制进行GPU渲染”开关)。

打开后可以根据不同的颜色观察UI上的Overdraw情况,蓝色、淡绿、淡红、深红代表4种不同程度的Overdraw情况,不同颜色的含义如下: 无色:没有过度绘制,每个像素绘制了1次。 蓝色:每个像素多绘制了1次。大片的蓝色还是可以接受的。如果整个窗口是蓝色的,可以尝试优化减少一次绘制。 绿色:每个像素多绘制了2次。 淡红:每个像素多绘制了3次。一般来说,这个区域不超过屏幕的1/4是可以接受的。 深红:每个像素多绘制了4次或者更多。严重影响性能,需要优化,避免深红色区域。 我们的目标是尽量减少红色Overdraw,看到更多的蓝色区域。

如何避免过度绘制

1.布局上的优化

在XML布局上,如果出现了过度绘制的情况,可以使用Hierarchy View来查看具体的层级情况,可以通过XML布局优化来减少层级。需要注意的是,在使用XML文件布局时,会设置很多背景,如果不是必需的,尽量移除。布局优化总结为以下几点: 移除XML中非必需的背景,或根据条件设置。

移除Window默认的背景。

按需显示占位背景图片。

使用Android自带的一些主题时,activity往往会被设置一个默认的背景,这个背景由DecorView持有。当自定义布局有一个全屏的背景时,比如设置了这个界面的全屏黑色背景,DecorView的背景此时对我们来说是无用的,但是它会产生一次Overdraw。因此没有必要的话,也可以移除,代码如下:

注意 针对 ListView 中的 Avatar ImageView 的设置,在 getView 的代码中,判断是否获取对应的 Bitmap,获取 Avatar的图像之后,把 ImageView 的 Background 设置为 Transparent,只有当图像没有获取到时,才设置对应的Background占位图片,这样可以避免因为给Avatar设置背景图而导致的过度渲染。

2.自定义View优化

事实上,由于我们的产品设计总是追求更华丽的视觉效果,仅仅通过布局优化很难做到最好,这时可以对复杂的控件使用自定义View来实现,虽然自定义View减少了Layout的层级,但在实际绘制时也是会过度绘制的。原因是有些过于复杂的自定义View(通常重写了onDraw方法), Android系统无法检测在onDraw中具体会执行什么操作,无法监控并自动优化,也就无法避免Overdraw了。

但是在自定义View中可以通过**canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。canvas.clipRect()可以很好地帮助那些有多组重叠组件的自定义View来控制显示的区域。clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制,并且可以使用canvas.quickreject()**来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。接下来介绍使用一个自定义View避免OverDraw的案例。

快速判断Canvas是否需要绘制:Canvas.QuickReject:

在绘制一个单元之前,首先判断该单元的区域是否在Canvas的剪切域内。若不在,直接返回,避免CPU和GPU的计算和渲染工作。

避免绘制越界:Canvas.ClipRect:

每个绘制单元都有自己的绘制区域,绘制前,Canvas.ClipRect(Region.Op.INTERSECT)帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内,才会被绘制,其他的区域被忽视。这个API可以很好地帮助那些有多组重叠组件的自定义View来控制显示的区域。clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。

本文参考:罗彧成的《Android应用性能优化最佳实践》一书。强烈推荐此书!

读《深入理解Java虚拟机》总结<三.java中的新生代和老年代内存>

概念

新生代:新生代中98%的对象都是朝生夕死 (1个eden 区(80%) 和 2个survivor区(10%))。 老年代:存活对象存留的时间比较久。

MinorGC:发生在新生代的垃圾回收。很频繁,速度快。 FullGC:发生在老年代的垃圾回收。发生FullGC通常会伴随一次MinorGC(并非绝对),FullGC会比MinorGc慢10倍以上。<!--more-->

内存分配与回收策略:

1.对象优先在Eden区进行分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次MinorGC

2.对象直接进入老年代

所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾回收以获取足够的连续空间来安置他们。

3.长期存活的对象将进入老年代

虚拟机既然采用了分代收集的思想来管理内存,那内存回收时就必须能识别哪些对象应当放在新生代,哪些对象应放在老年代。为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当他的年龄增加到一定程度(默认15)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过设置参数-XX:MaxTenuringThreshold。

4 动态对象年龄判定

为了更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

5 空间分配担保

在发生MinorGC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败:如果允许,那只会进行MinorGC;如果不允许,则也要改为进行一次FullGC。取平均值进行比较其实仍然是一种动态概率的手段,也就是说如果某次MinorGC存活后的对象突增,远远高于平均值得话,依然会导致担保失败。如果出现了担保失败,那只好在失败后重新发起FullGC。虽然担保失败时绕的圈子是最大的,但大部分情况下还是会将HandlePromotionFailure 开关打开,避免Full GC过于频繁。

Android layout_weight 计算方式

假设:LinearLayout为android:orientation="horizontal", layout_weight属性值分别为1、2、2

第一种情况:每个控件的宽度属性都为android:layout_width="0dp",那么额外的空为(手机的宽度假设为X)X-0-0-0=X,那么根据sdk上所述:<!--more--> 第一个控件的宽度为 0+(1/(1+2+2))*X=X/5 第二个控件的宽度为 0+(2/(1+2+2))*X=2X/5 第三个控件的宽度为 0+(2/(1+2+2))*X=2X/5

第二种情况:每个控件的宽度属性都为android:layout_width="match_parent",那么额外的空间就是X-X-X-X=-2X 第一个控件的宽度为 X+(1/(1+2+2))(-2X)=3X/5 第二个控件的宽度为 X+(2/(1+2+2))(-2X)=X/5 第三个控件的宽度为 X+(2/(1+2+2))*(-2X)=X/5

第三种情况:其中第一个控件宽度属性为android:layout_width="match_parent",其他两个控件宽度为android:layout_width="0dp",额外的空间为X-X-0-0=0 第一个控件的宽度为 X+(1/(1+2+2))*0=X 第二个控件的宽度为 0+(2/(1+2+2))*0=0 第三个控件的宽度为 0+(2/(1+2+2))*0=0

最终结论:layout_weight的公式 控件的宽度/高度 = 控件的width/height值+(该控件的weight值/所有控件的weight的和)×额外的空间 额外的空间=手机的宽度/高度-所有控件的宽度/高度

注:如果属性为android:layout_width="wrap_content" 则计算时,总宽/高要减去控件本身占据的宽/高

转自:https://blog.csdn.net/Dazlly/article/details/13767343

Android屏幕适配 px,dp,dpi及density的关系与深入理解

PX(pixel):

即传统计算机语言中描述的像素,在Android则代表绝对像素。

之所以Android中不推荐使用这种单位,正是因为不同生产厂商,不同品牌,不同屏幕的设备,其分辨率亦不一。

举例来说,我们现在将某个Button的width设为160px,则会出现如下情况:

在分辨率为“320宽”的设备里,该按钮显示占屏幕宽度一半;

在分辨率为“640宽”的设备里,该按钮显示占屏幕宽度的四分之一;

<!--more-->

DPI(Dots Per Inch):

为了避免上面说到的使用px在屏幕适配中带来的问题,Android引入了一个新的单位:dp/dip。

而在理解“dp”之前,我们更有必要先了解一下另一个概念。正是:dpi。

也有人讲dpi称为“屏幕密度”。其含义则是:每英寸所打印的点数,既每一英寸的屏幕所包含的像素数。

举例来说,假设现在有一台“宽2英寸,长3英寸”的设备,则:

当该设备分辨率为“320x480”,则dpi值为160。 当该设备分辨率为“640x960”,则dpi值为320。 而“dpi”值越高的设备,其屏幕显示画面的效果也就越精细。

使用场景:

正是因为dpi值其代表的特性,所以android项目的资源文件下存在以下目录:

drawable-ldpi ( 当dpi为120时,使用此目录下的资源) drawable-mdpi ( 当dpi为160时,使用此目录下的资源) drawable-hdpi ( 当dpi为240时,使用此目录下的资源) drawable-xhdpi ( 当dpi为320时,使用此目录下的资源) drawable-xxhdpi ( 当dpi为480时,使用此目录下的资源) Android正是根据设备DPI值得不同,选择清晰度不同的资源使用,完成屏幕的适配。

DP/DIP(device independent pixels):

与我们之前谈到的绝对密度“px”对应,Android中引入的“dp”代表的则是“设备独立像素”。

该单位是为支持WVGA、HVGA和QVGA而使用的,其不再依赖像素本身,而是和屏幕密度相关。

在Android当中规定:在屏幕密度为“160dpi”的情况下,则刚好“1dp = 1px”。

注:当屏幕密度为“320dpi”时,则“1dp = 2px”,以此类推.......

也正是因此,让我们得以保证了:控件在不同密度的屏幕上显示一致,既完成屏幕适配。

使用场景:

让我们回到上面说到的使用px造成的控件显示问题,此时我们将使用新的单位“dp”。于是:

在分辨率320x480(既dpi为160)的设备下,则160dp等价于160px,按钮占屏幕宽的一半。 在分辨率640x960(既dpi为320)的设备下,则160dp等价于320px,按钮依然占屏幕宽的一半。

Density:

就这个单词本身直接翻译的意思而言,其也代表“密度”。但需要注意的是,在Android中,其实并非如此。 注意我们这里指的是,通过代码“context.getResources().getDisplayMetrics().density”获取的“density”值。 而通过该方法获取到的该值,实际上是等价于“dpi / 160”的一个结果值。也就是说: “getResources().getDisplayMetrics().density” = “getResources().getDisplayMetrics().densityDpi / 160”

看到这样一个解析,聪明的人大概已经能预见什么了。我们似乎发现了某种关联: 在Android里:“dpi = 160,则1dp = 1px”、“dpi = 320,则1dp = 2px”。以此类推。 到此你已经发现,dp,px与160之间存在着某种规律:“1dp = (dpi / 160)px” 换算一下,最终得到公式: dp = density * px

到了这里我们明白了,其实Android提供的该值,也就是为了让我们在dp与px之间做转换。 归根结底,其目的还是为了帮助我们做屏幕适配。

使用场景: 虽然使用dp在xml文件中定义控件尺寸,能够很好的帮助我们完成适配。 但很多时候,我们也会需要在Java代码中动态的去设定控件的尺寸。

但由于在代码中的尺寸设定,基本都被默认为了px单位。 所以这个时候就可以借助“density”来帮我们完成dp与px的转换,从而完成适配。

1
2
3
4
public static int dip2px(Context context, float dipValue){ 
final float scale = context.getResources().getDisplayMetrics().density;
return (int)(dipValue * scale + 0.5f);
}

1
2
3
4
public static int px2dip(Context context, float pxValue){ 
final float scale = context.getResources().getDisplayMetrics().density;
return (int)(pxValue / scale + 0.5f);
}

原文链接:https://blog.csdn.net/ghost_Programmer/article/details/50042805

Android Studio 配置 JNI

JNI 是java 语言调用C/C++函数的接口。与JNI相关的开发工具包是NDK。下面记录Android Studio 里用 java代码调用 JNI的基本过程。当前环境:

Android Studio:3.1.2.

NDK:20.0.5<!--more-->

准备

下载NDK:

###1.创建Java类

定义一个工具类,里面定义了native方法:

1
2
3
4
5
6
7
8
9
package cn.qiracle.jnidemo.jni;

public class JniUtils {

static {
System.loadLibrary("JniLib");
}
public static native String getJniString();
}

2.创建JNI文件夹

在main文件夹下创建jni文件夹:

3.生成头文件

两种方式:

1.通过命令行

javac JniUtils.java 编译生成 JniUtils.class文件。然后回退到包外目录javah -jni cn.qiracle.jnidemo.jni.JniUtils。将生成的.h文件移动到main下的jni目录下。

2.配置外部工具

点击File - Setting - Tools - External Tools 打开外部工具配置页,点击 + 新建一个工具。

Program: $JDKPath$\bin\javah.exe

Arguments: -classpath . -jni -d $ModuleFileDir$\src\main\jni $FileClass$

Working directory: $ModuleFileDir$\src\main\Java

点击 OK 保存后就新建了一个工具。此时我们右击 JniUtils.java,在菜单中选择 External Tools - javah 就可以快速生成头文件并放到 jni 目录。

4.编写C代码

在jni目录下新建一个cn_qiracle_jnidemo_jni_JniUtils.cpp文件。内容如下:

1
2
3
4
5
6
7
#include "cn_qiracle_jnidemo_jni_JniUtils.h"

JNIEXPORT jstring JNICALL Java_cn_qiracle_jnidemo_jni_JniUtils_getJniString
(JNIEnv *env,jclass jobj) {

//new 一个字符串,返回Hello World
return (*env).NewStringUTF("Hello jni");
}

这里输出 hello jni

5.创建mk文件

mk 文件用于告诉 ndk-build 该如何编译 c 源码。详情见官方指南

在jni目录下创建Android.mk:

1
2
3
4
5
6
7
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := JniLib
LOCAL_SRC_FILES =: cn_qiracle_jnidemo_jni_JniUtils.cpp
include $(BUILD_SHARED_LIBRARY)

创建 Application.mk

1
2
APP_MODULES := JniLib
APP_ABI := all

6.gradle配置

在 module 的 build.gradle 里,amndroid.defaultConfig 下加入下面配置:

1
2
3
4
5
6
7
ndk {
moduleName "JniLib"
}
sourceSets.main{
jni.srcDirs = []
jniLibs.srcDir "src/main/libs"
}

6.编译

这里我们配置使用外部工具:

  • Program: D:\SDK\ndk-bundle\build\ndk-build.cmd (注意这里是ndk-build路径)
  • Working directory: $ProjectFileDir$\app\src\main

任意找个第地方右击,选择 External Tools - ndk-build 即可编译 c 源码。成功后可以看见创建了 libs 目录,里面包含了不同平台下的 so 文件。

7.验证

在MainActivity.java里调用native方法:

1
Toast.makeText(this,JniUtils.getJniString(),Toast.LENGTH_SHORT).show();

运行结果:

参考:

https://www.jianshu.com/p/09ff3300f453

Android AIDL的基本用法

AIDL 是 Android 中实现跨进程通信的一种方式。下面是一个简单的实现案例,通过 AIDL 实现客户端调用服务端的接口, 实现跨进程通信。<!--more-->

Server

首先,创建一个服务端工程 AIDLServer。main下面创建aidl文件夹,然后在aidl文件夹下创建包和aidl文件,如下:

IMyAidlInterface.aidl 文件内容如下:

1
2
3
4
5
6
7
8
9
// IMyAidlInterface.aidl
package cn.qiracle.aidlserver;

// Declare any non-default types here with import statements

interface IMyAidlInterface {
void print();
int add(int a, int b);
}

这里定义了AIDL接口IMyAidlInterface,接口里定义了两个待实现的方法。然后 重新构建下工程,这样在build目录下就会生成IMyAidlInterface.java 文件。

接着定义一个Service,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package cn.qiracle.aidlserver;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class MyService extends Service {
public MyService() {
}

@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}

class MyBinder extends IMyAidlInterface.Stub {

@Override
public void print() throws RemoteException {
Log.i("qiracle", "my aidl test");
}

@Override
public int add(int a, int b) throws RemoteException {
Log.i("qiracle", "a + b:" + (a + b));
return a + b;
}
}
}

在这个Service里 ,内部类MyBinder继承IMyAidlInterface.Stub ,然后实现接口里待实现的方法。这两个方法就是提供给客户端进行调用的。

最后,清单文件里记得注册下服务

1
2
3
4
5
6
7
8
<service
android:name=".MyService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="cn.qiracle.aidlservice" />
</intent-filter>
</service>

然后启动服务

1
2
3
4
Intent intent = new Intent();
intent.setAction("cn.qiracle.aidlservice");
intent.setPackage("cn.qiracle.aidlserver");
startService(intent);

以上就是AIDL的服务端简单实现。主要就是定义一个AIDL接口,然后在服务里实现这个接口,提供给客户端调用。下面介绍客户端部分。

Client

先建立一个客户端的工程,跟服务端工程一样,需要在main下建立aidl目录,然后在aidl目录下新建包和aidl文件,这里的包名和aidl文件名必须和服务端保持一致。如下:

IMyAidlInterface.aidl 的内容也和 服务端的 IMyAidlInterface.aidl 保持一致

然后,在Activity里绑定服务,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Intent intent = new Intent();
intent.setAction("cn.qiracle.aidlservice");
intent.setPackage("cn.qiracle.aidlserver");

bindService(intent, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);
}

@Override
public void onServiceDisconnected(ComponentName name) {

}
},BIND_AUTO_CREATE);

可以通过按钮的点击事件,调用服务端的方法。

1
2
3
4
5
6
7
8
9
public void click(View view){
Toast.makeText(getApplicationContext(),"click",Toast.LENGTH_SHORT).show();
try {
iMyAidlInterface.print();
iMyAidlInterface.add(1,2);
} catch (RemoteException e) {
e.printStackTrace();
}
}

以上就是客户端实现。

开始运行,注意需要先运行服务端,开启Service,然后运行客户端,绑定服务,通过点击按钮,可以看到服务端控制台有日志打印。

以上,我们就通过AIDL实现了跨进程的调用,方法的实现在服务端,方法的调用在客户端。

Android 知识点总结(一)

Handler

  1. Handler 的回调方法是在 Looper.loop()所调用的线程进行的;

  2. Handler 的创建需要先调用 Looper.prepare() ,然后再手动调用 loop()方法开启循环;

  3. App 启动时会在ActivityThread.main()方法中创建主线程的 Looper ,并开启循环,所以主线程使用 Handler 不用调用第2点的逻辑;<!--more -->

  4. 延时消息并不会阻塞消息队列;

  5. 异步消息不会马上执行,插入队列的方式跟同步消息一样,唯一的区别是当有消息屏障时,异步消息可以继续执行,同步消息则不行;

  6. Callback.handleMessage() 的优先级比 Handler.handleMessage()要高*

  7. Handler.post(Runnable)传递的 Runnale 对象并不会在新的线程执行;

  8. Message 的创建推荐使用 Message.obtain() 来获取,内部采用缓存消息池实现;

  9. 不要在 handleMessage()中对消息进行异步处理;

  10. 可以通过removeCallbacksAndMessages(null)或者静态类加弱引用的方式防止内存泄漏;

  11. Looper.loop()不会造成应用卡死,里面使用了 Linux 的 epoll 机制。

Context

源码中的注释是这么来解释Context的:Context提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。就是说,它描述一个应用程序环境的信息(即上下文);是一个抽象类,Android提供了该抽象类的具体实现类;通过它我们可以获取应用程序的资源和类(包括应用级别操作,如启动Activity,发广播,接受Intent等)。既然上面Context是一个抽象类,那么肯定有他的实现类咯,我们在Context的源码中通过IDE可以查看到他的子类最终可以得到如下关系图:

Context类本身是一个纯abstract类,它有两个具体的实现子类:ContextImpl和ContextWrapper。其中ContextWrapper类,如其名所言,这只是一个包装而已,ContextWrapper构造函数中必须包含一个真正的Context引用,同时ContextWrapper中提供了attachBaseContext()用于给ContextWrapper对象中指定真正的Context对象,调用ContextWrapper的方法都会被转向其所包含的真正的Context对象。ContextThemeWrapper类,如其名所言,其内部包含了与主题(Theme)相关的接口,这里所说的主题就是指在AndroidManifest.xml中通过android:theme为Application元素或者Activity元素指定的主题。当然,只有Activity才需要主题,Service是不需要主题的,因为Service是没有界面的后台场景,所以Service直接继承于ContextWrapper,Application同理。而ContextImpl类则真正实现了Context中的所以函数,应用程序中所调用的各种Context类的方法,其实现均来自于该类。一句话总结:Context的两个子类分工明确,其中ContextImpl是Context的具体实现类,ContextWrapper是Context的包装类。Activity,Application,Service虽都继承自ContextWrapper(Activity继承自ContextWrapper的子类ContextThemeWrapper),但它们初始化的过程中都会创建ContextImpl对象,由ContextImpl实现Context中的方法。

一个应用程序有几个Context?

实这个问题本身并没有什么意义,关键还是在于对Context的理解,从上面的关系图我们已经可以得出答案了,在应用程序中Context的具体实现子类就是:Activity,Service,Application。那么Context数量=Activity数量+Service数量+1。当然如果你足够细心,可能会有疑问:我们常说四大组件,这里怎么只有Activity,Service持有Context,那Broadcast Receiver,Content Provider呢?Broadcast Receiver,Content Provider并不是Context的子类,他们所持有的Context都是其他地方传过去的,所以并不计入Context总数。上面的关系图也从另外一个侧面告诉我们Context类在整个Android系统中的地位是多么的崇高,因为很显然Activity,Service,Application都是其子类,其地位和作用不言而喻。

视图

Activity

Activity并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的是Window。一个Activity包含了一个Window,Window才是真正代表一个窗口。Activity就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与Window、以及View进行交互。

Window

Window是视图的承载器,内部持有一个 DecorView,而这个DecorView才是 view 的根布局。Window是一个抽象类,实际在Activity中持有的是其子类PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView来加载Activity中设置的布局R.layout.activity_main。Window 通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图绘制以及其他交互。

DecorView

DecorView是FrameLayout的子类,它可以被认为是Android视图树的根节点视图。DecorView作为顶级View,一般情况下它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下三个部分,上面是个ViewStub,延迟加载的视图(应该是设置ActionBar,根据Theme设置),中间的是标题栏(根据Theme设置,有的布局没有),下面的是内容栏。

ViewRoot

ViewRoot可能比较陌生,但是其作用非常重大。所有View的绘制以及事件分发等交互都是通过它来执行或传递的。

ViewRoot对应ViewRootImpl类,它是连接WindowManagerService和DecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot来完成。

ViewRoot并不属于View树的一份子。从源码实现上来看,它既非View的子类,也非View的父类,但是,它实现了ViewParent接口,这让它可以作为View的名义上的父视图。RootView继承了Handler类,可以接收事件并分发,Android的所有触屏事件、按键事件、界面刷新等事件都是通过ViewRoot进行分发的。

要知道,当用户点击屏幕产生一个触摸行为,这个触摸行为则是通过底层硬件来传递捕获,然后交给ViewRootImpl,接着将事件传递给DecorView,而DecorView再交给PhoneWindow,PhoneWindow再交给Activity,然后接下来就是我们常见的View事件分发了。

硬件 -> ViewRootImpl -> DecorView -> PhoneWindow -> Activity

通过以上了解可以知道,Activity就像个控制器,不负责视图部分。Window像个承载器,装着内部视图。DecorView就是个顶层视图,是所有View的最外层布局。ViewRoot像个连接器,负责沟通,通过硬件的感知来通知视图,进行用户之间的交互。

Android Boardcast 权限的使用

1. 广播的接收权限

设置广播接收权限的目的在于避免自己应用发送的广播被其他恶意应用接收到。简单来说就是定义谁能接收我的广播,用法如下:

首先发送方的应用在清单文件里自定义一个权限:

1
2
<permission android:name="cn.qiracle.RECEIVER"
android:protectionLevel="signature" />

这里 protectionLevel 选择 signature 或者 signatureOrSystem 更加安全。<!--more-->

然后发送广播时调用如下 sendBoradcast 方法:

1
2
3
4
private static final String PERMISSION_RECEIVER= "cn.qiracle.RECEIVER";
...
...
sendBroadcast(intent,PERMISSION_RECEIVER);

此时接收方app若是想接收到这个广播,需要在接收方应用的清单文件里添加如下权限:

1
<uses-permission android:name="cn.qiracle.RECEIVER" />

2.广播的发送权限

设置广播的发送权限目的在于避免自己的应用里的 receiver 被其他恶意应用发送的带有同样 action 的广播所骚扰。简单来说就是定义谁能给我发送广播。用法如下:

首先在接收方应用的清单文件里自定义一个权限:

1
2
<permission android:name="cn.qiracle.SEND"
android:protectionLevel="signature" />

然后注册广播时采用如下regiserReceiver方法:

1
2
3
4
private static final String PERMISSION_SEND = "cn.qiracle.SEND";
...
...
registerReceiver(new MyReceiver(),intentFilter,PERMISSION_SEND,null);

上面是动态注册,静态注册广播方式如下:

1
2
3
4
5
6
<receiver android:name=".receiver.MyReceiver"
android:permission="cn.qiracle.SEND">

<intent-filter>
<action android:name="cn.qiracle.MYRECEIVER"/>
</intent-filter>
</receiver>

推荐使用动态注册的方式,因为笔者尝试发现自定义权限时,静态注册的方式在 **Android8.0 **及以上会不起作用。

此时广播的发送方 app 要想给这个应用发送广播,必须在清单文件里添加如下权限:

1
<uses-permission android:name="cn.qiracle.SEND" />

读《深入理解Java虚拟机》总结<二.java内存模型与线程>

主内存与工作内存

java内存模型规定了所有变量都存储在主内存中。此处的变量是指实例字段,静态字段和构成数组对象的元素。但不包括局部变量与方法参数。因为后者是线程私有的,不会被共享。除主内存之外,每个线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法之间访问对方工作内存中的变量。线程间变量值得传递均需要通过主内存来完成。 <!--more-->

一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存之类的实现细节,java内存模型定义了以下八种操作来完成:

  • lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
  • unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
  • write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中

java内存模型

volatile 关键字

用于修饰变量。主要作用有两个:

1.保证修改的可见性

2.禁止指令重排序

volatile不保证操作的原子性

读《深入理解Java虚拟机》总结<一自动内存管理机制>

这一周来比较空闲,读了《深入理解java虚拟机一书》以提高自己对java底层的认知,还没看完,只是挑选了书中自己比较感兴趣的两个章节来看,写下此篇博客一是为了总结,二是为了方便今后回顾。下面是第一部分自动内存管理机制 <!--more-->

运行时数据区域

java内存

程序计数器

程序计数器时一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程直接的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

java虚拟机栈

与程序计数器一样,java虚拟机栈也是线程私有的,它的生命周期与线程相同。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackoverflowError异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。。本地方法栈也会抛出StackoverflowError和OutOfMemoryError异常。

java堆

对于大多数应用来说,java堆是java虚拟机所管理的内存中最大的一块。java堆是所以线程共享的一块内存区域,在虚拟机启动时创建。java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配,并且堆无法再扩展时,将会抛出OutOfMemoryError异常。

方法区

方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。垃圾回收行为在这个区域是比较少出现的,这个区域内存回收目标主要是针对常量池的回收和对类型的卸载。。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池

运行时常量池时方法区的一部分。Class文件除了有类的版本,字段,方法,接口等描述信息外,还有一项信息就是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区运行时常量池中。运行期间也可能将新的常量放入池中,如String类的intern()方法。

确定对象是否存活的算法

垃圾回收器在对堆进行回收前,第一件事情就是要确定这些对象有哪些还存活着,哪些已经死去。

引用计数算法

给对象中添加一个引用计算器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。。

java语言没有选用引用计数法来管理内存,其中最主要的原因是它很难解决对象之间的相互循环引用的问题。如下:

1
2
3
4
5
6
ReferenceCountGC objA = new ReferenceCountGC();
ReferenceCountGC objB = new ReferenceCountGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;

根搜索算法

在主流的商用程序语言中,都是使用根搜索算法判断对象是否存活的。基本思路是通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象待GC Roots没有任何引用链相连,则证明此对象是不可用的。

Java语言里,可作为GC Roots对象包括下面几种:

  • 虚拟机栈中引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI的引用对象

java中的四种引用

**强引用:**代码至中普遍存在。类似 Object obj = new Object()。主要强引用还在,垃圾回收器永远不会回收掉被引用的对象。

**软引用:**当内存不够时,即系统将要发生内存溢出异常之前,将会把这些对象列进回收范围并进行二次回收。java中提供SoftReference类实现软引用。

**弱引用:**被弱引用关联的对象只能生存到下次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。java中提供WeakReference类实现软引用。

虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望这个对象在被收集器回收时收到一个系统通知。java中提供PhantomReference类实现软引用。

对象死亡过程

在跟搜索算法中不可达的对象也并非是非死不可的。这些不可达的对象先会被判断是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机会将这两种情况都视为“没有必要执行”。

finalize()方法是对象逃脱死亡命运的最后一次机会,如果对象想要在finalize()方法中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可。譬如把自己(this)赋值给某个类变量或者某个对象的成员变量。如下:

1
2
3
4
5
6
7
8
9
10
public class FinalizeEscapeGc{
public static FinalizeEscapeGc Save_HooK = null;

@Override
protected void finalized() throws Throwable{
super.finalized();
FinalizeEscapeGc.Save_HooK = this;
}

}

如何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会再次被执行。

垃圾回收算法

标记-清除算法

首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。它的主要缺点有两个:

一是效率问题,标记和清楚过程效率都不高

二是空间问题,标记清除后会产生大量不连续的内存碎片。

标记-清除

复制算法

它将可用内存按容量划分为大小相同的两块,每次只使用其中的一块,当一块内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这种算法的代价是将内存缩小为原来的一半未免太高了一些。

复制

现在的商用虚拟机都采用这种收集算法来回收新生代,IBM的专门研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分空间。而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性拷贝到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1。当Survivor空间不够时,需要依赖其他内存(老年代)进行分配担保。

标记-整理算法

复制算法在对象存活率较高时需要执行较多的复制操作,更关键是如果不想浪费50%空间,就需要额外的空间进行担保,以应对内存中所有对象都100%存活的极端情况,所以老年代一般不能直接选用这种算法。

标记-整理算法的标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。

标记-整理

分代收集算法

根据对象的存活周期不同将内存划分为几块,一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时发现大批对象死去,只有少量存活,那就选用复制算法。老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记-清理”,“标记-整理”算法来进行回收。

垃圾收集器

垃圾收集器

如果两个收集器之间存在连线,就说明他们可以搭配使用。下面就只说下Serial收集器:

Serial收集器

Serial收集器是最基本,历史最悠久的收集器,这是一个单线程的收集器。它在进行垃圾收集时,必须暂停其他所有工作线程指到它收集结束。

|