要讲启动模式,先讲讲任务栈Task
,它是一种用来放置Activity
实例的容器,他是以栈的形式进行盛放,也就是所谓的先进后出,主要有2个基本操作:压栈
和出栈
,其所存放的Activity
是不支持重新排序
的,只能根据压栈和出栈操作更改Activity
的顺序。
启动一个Application
的时候,系统会为它默认创建一个对应的Task
,用来放置根Activity
。默认启动Activity
会放在同一个Task中,新启动的Activity
,如果拥有相同affinity
会放在同一个Task中,并且显示它
affinity
是Activity
内的一个属性,在ManiFest
中对应属性为taskAffinity
,可以手动修改,默认情况下,所有的Activity
的affinity
都从Application
继承,为Manifest
的包名
。
当用户按下回退键时,这个Activity
就会被弹出栈,按下Home
键回到桌面,再启动另一个应用,这时候之前那个Task
就被移到后台,成为后台任务栈,而刚启动的那个Task
就被调到前台,成为前台任务栈,Android
系统显示的就是前台任务栈中的栈顶实例Activity
。
栈是一个先进后出的线性表,根据Activity
在当前栈结构中的位置,来决定该Activity
的状态。当然,世界不可能一直这么“和谐”,可以给Activity
设置一些“特权”,来打破这种“和谐”的模式,这种特权,就是通过在AndroidManifest
文件中的属性andorid:launchMode
来设置或者通过Intent
的flag
来设置的,下面就先介绍下Activity
的几种启动模式。
默认模式:可以不用写配置。在这个模式下,都会默认创建一个新的实例。因此,在这种模式下,可以有多个相同的实例,也允许多个相同Activity
叠加。
使用场景:绝大多数Activity。
栈顶复用模式:如果要开启的Activity
在任务栈的顶部已经存在,就不会创建新的实例,而是调用 onNewIntent()
方法。如果不位于栈顶,就会创建新的实例,避免栈顶的Activity
被重复的创建。
使用场景:在通知栏点击收到的通知,然后需要启动一个
Activity
,这个Activity
就可以用singleTop
,否则每次点击都会新建一个Activity
。当然实际的开发过程中,测试妹纸没准给你提过这样的bug
:某个场景下连续快速点击,启动了两个Activity
。如果这个时候待启动的Activity使用singleTop
模式也是可以避免这个Bug
的。
栈内复用模式: Activity
只会在一个任务栈里面存在一个实例。如果要激活的Activity
,在任务栈里面已经存在,就不会创建新的Activity
,而是复用这个已经存在的Activity
,调用 onNewIntent()
方法,并且清空这个Activity
任务栈上面所有的Activity
。singleTask
模式和前面两种模式的最大区别就是singleTask
模式是任务内单例的,在不同的任务栈可以存在多个实例。
使用场景:大多数
App
的主页。对于大部分应用,当我们在主界面点击回退按钮的时候都是退出应用,那么当我们第一次进入主界面之后,主界面位于栈底,以后不管我们打开了多少个Activity
,只要我们再次回到主界面,都应该使用将主界面Activity
上所有的Activity
移除的方式来让主界面Activity处于栈顶,而不是往栈顶新加一个主界面Activity
的实例,通过这种方式能够保证退出应用时所有的Activity
都能销毁。
单一实例模式:整个手机操作系统里面只有一个实例存在。在该模式下,我们会为目标Activity
分配一个新的affinity
,并创建一个新的Task
栈,并且任务栈里面只有他一个实例存在。不同的应用去打开这个Activity
共享公用的同一个Activity
。
应用场景:你会发现它启动时会慢一些,切换效果不好,影响用户体验。它往往用于多个应用之间,例如一个电视
Launcher
里的Activity
,通过遥控器某个键在任何情况可以启动,当在某应用中按键启动这个Activity,处理完后按返回键,就会回到之前启动它的应用,不影响用户体验。还有呼叫来电界面等
我们说的动态设置,其实是通过Intent
。如果我们要设置要启动的Activity
的启动模式的话,只需要这样:
intent.setFlags(Intent.FLAG_ACTIVITY_XXX);
在给目标Activity
设立此Flag
后,会根据目标Activity
的affinity
进行匹配:如果已经存在与其affinity
相同的Task
,则将目标Activity
压入此Task
。反之没有的话,则新建一个Task
,新建的Task
的affinity
值与目标Activity
相同。然后将目标Ac
tivity压入此栈。有两点注意的地方:
Task
没有说只能存放一个目标Activity
。只是说决定是否新建一个Task
。Activity
都是默认的affinity
,那么此Flag
无效。使用场景:该
Flag
通常使用在从Service
中启动Activity
的场景,由于Service
中并不存在Activity
栈,所以使用该Flag
来创建一个新的Activity
栈,并创建新的Activity
实例。
使用singletop
模式启动一个Activity
,与指定android:launchMode=“singleTop”
效果相同。
当设置此Flag
时,目标Activity
会检查Task
中是否存在此实例,如果没有则添加压入栈,如果有,就将位于Task
中的对应Activity
其上的所有Activity
弹出栈,接下来的操作有以下两种情况:
Flag_ACTIVITY_SINGLE_TOP
,则直接复用栈内的对应Activity,并调用onNewIntent()
;Flag_ACTIVITY_SINGLE_TOP
,则将栈内的对应Activity
销毁重新创建。关于这个Flag
,我们发现他和singleTask
很像,但却有区别,不指定FLAG_ACTIVITY_SINGLE_TOP
的话他会销毁已存在的目标实例再重新创建。准确的说,singleTask
= FLAG_ACTIVITY_CLEAR_TOP
+FLAG_ACTIVITY_SINGLE_TOP
。
Activity使用这种模式启动Activity,当该Activity启动其他Activity后,该Activity就消失了,不会保留在Activity栈中。
onCreate(Bundle savedInstanceState)->onStart()->onResume()->onPause()->onStop()->onDestory()
onCreate(Bundle savedInstanceState)
系统首次创建 Activity
时触发。Activity
会在创建后进入已创建状态。在 onCreate
() 方法中,您需执行基本应用启动逻辑,该逻辑在 Activity
的整个生命周期中只应发生一次。
使用场景:
onCreate
() 的实现可能会将数据绑定
到列表,将Activity
与ViewModel
相关联,并实例化某些类范围变量。此方法接收savedInstanceState
参数,后者是包含Activity
先前保存状态的Bundle
对象。如果Activity
此前未曾存在,则Bundle
对象的值为null
。
onStart()
当 Activity
进入“已开始”状态时,系统会调用此回调。onStart()
调用使 Activity
对用户可见,只是还没有在前台显示,没有焦点,因此用户也无法交互。因为应用会为 Activity
进入前台并支持交互做准备。
使用场景:应用通过此方法来初始化维护界面的代码。比如注册一些变量。这些变量必须保证
Activity
在前台的时候才能够被响应。因为生命周期较短,不要做一些耗时操作,也不要做一些与UI
相关的。
onResume()
Activity
会在进入“已恢复”状态时来到前台,然后系统调用 onResume()
回调。这是应用与用户交互的状态,Activity
已在在屏幕上显示UI并获得了焦点。应用会一直保持这种状态,直到某些事件发生,让焦点远离应用。
使用场景:可以启动任何需要在组件可见,且位于前台时运行的功能,例如启动摄像头预览与开启动画。
onPause()
系统将此方法视为用户正在离开您的 Activity
的第一个标志(尽管这并不总是意味着活动正在遭到销毁);此方法表示 Activity
不再位于前台(尽管如果用户处于多窗口模式,Activity
仍然可见)。
使用场景:可以停止任何无需在组件未在前台时运行的功能,例如停止摄像头预览与停止动画。
onPause
方法执行完成后,新Activity
的onResume
方法才会被执行。所以onPause
不能太耗时,因为这可能会影响到新的Activity
的显示。
onStop()
如果您的 Activity
不再对用户可见,则说明其已进入已停止状态,因此系统将调用 onStop
() 回调。举例而言,如果新启动的 Activity
覆盖整个屏幕,就可能会发生这种情况。如果系统已结束运行并即将终止,系统还可以调用 onStop
()。需要注意的是:在内存不足而导致系统自动回收进程情况下,onStop()
可能都不会被执行。
使用场景:反注册在
onStart
函数中注册的变量。您还应该使用onStop()
执行CPU
相对密集的关闭操作。例如,如果您无法找到更合适的时机来将信息保存到数据库,则可在onStop()
期间执行此操作。
onDestroy()
销毁 Activity
之前,系统会先调用 onDestroy()
。
使用场景:
onDestroy()
回调应释放先前的回调(例如onStop()
)尚未释放的所有资源。
onRestart
不属于正常下得生命周期。此方法回调时,表示Activity
正在重新启动,由不可见状态变为可见状态。这种情况,一般发生在用户打开了一个新的Activity
时,之前的Activity
就会被onStop
,接着又回到之前Activity
页面时,之前的Activity
的 onRestart
方法就会被回调,接着走onStart
以后的生命周期。
在
onResume()
一般会打开独占设备,开启动画等,当需要从AActivity
切换到BActivity
时,先执行AActivity
中的onPause()
进行关闭独占设备,关闭动画等,以防止BActivity
也需要使用这些资源,因为AActivity
的资源回收,也有利于BActivity
运行的流畅。
04-17 20:54:46.997: I/com.yhd.test.AActivity(5817): onPause()
04-17 20:54:47.021: I/com.yhd.test.BActivity(5817): onCreate()
04-17 20:54:47.028: I/com.yhd.test.BActivity(5817): onStart()
04-17 20:54:47.028: I/com.yhd.test.BActivity(5817): onResume()
04-17 20:54:47.099: I/com.yhd.test.AActivity(5817): onStop()
04-17 20:54:48.070: I/com.yhd.test.AActivity(5817): onDestroy()
官方解释:当Activity
被设以singleTop模式(包含FLAG_ACTIVITY_SINGLE_TOP
)启动,当需要再次响应此Activity
启动需求时而且该Activity
处于栈顶,会复用栈顶的已有Activity
,还会调用onNewIntent
方法。
onNewIntent
设计初衷是为了Activity
的复用,与onCreate
只能二选一。其实除了singleTop
启动模式,还有singleTask
和singleInstance
。下面分三种情况讨论会调用onNewIntent
的情形:
ActivityA
,这时会调用onNewIntent
()方法 ,生命周期顺序为:onPause--->onNewIntent--->onResume
ActivityA
,这时会调用onNewIntent
()方法 ,生命周期顺序为:onNewIntent--->onRestart--->onStart--->onResume
更准确的说法是,只对SingleTop
(且位于栈顶),SingleTask
和SingleInstance
(且已经在任务栈中存在实例)的情况下,再次启动它们时才会调用,即只对startActivity
有效,对仅仅从后台切换到前台而不再次启动的情形,不会触发onNewIntent
。
需要注意的是:如果我们在
onNewIntent
中不执行setIntent(intent)
,那么getIntent()得到的intent永远是上次旧的。
如果系统由于系统约束(不是正常的应用程序行为,如内存不足、用户直接按Home
和Menu
键以及屏幕旋转)而破坏了Activity
,onSaveInstanceState()
就会调用,如果Activity
重新创建才会执行和onRestoreInstanceState()
。重建的时候这些UI
的状态会默认保存,但是前提条件是UI
控件必须制定id
,如果没有指定id
的话,UI
的状态是无法保存的。
04-16 15:32:35.243 1741/com.yhd.test E/TAG-: onPause
04-16 15:32:35.304 1741/com.yhd.test E/TAG-: onSaveInstanceState
04-16 15:32:35.309 1741/com.yhd.test E/TAG-: onStop
04-16 15:36:01.427 1741/com.yhd.test E/TAG: onPause
04-16 15:36:01.427 1741/com.yhd.test E/TAG: onSaveInstanceState
04-16 15:36:01.433 1741/com.yhd.test E/TAG: onStop
04-16 15:36:01.433 1741/com.yhd.test E/TAG: onDestroy
04-16 15:36:01.454 1741/com.yhd.test E/TAG: onCreate
04-16 15:36:01.465 1741/com.yhd.test E/TAG: onStart
04-16 15:36:01.465 1741/com.yhd.test E/TAG: onRestoreInstanceState
04-16 15:36:01.468 1741/com.yhd.test E/TAG: onResume
当您的Activity由于系统约束开始停止时,系统会调用onSaveInstanceState()
以便您的Activity
可以使用一组键值对来保存状态信息。此方法的默认实现保存有关Activity
视图层次结构状态的信息(如果想要保存具体内容需要有id
)。为了保存Activity
的附加状态信息,您必须实现onSaveInstanceState()
并向对象添加键值对Bundle
。
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
// 保存用户自定义的状态
savedInstanceState.putString("KEY", object);
// 调用父类交给系统处理,这样系统能保存视图层次结构状态以及内容信息
super.onSaveInstanceState(savedInstanceState);
}
当您的Activity
由于系统约束重新创建时,您可以从Bundle
系统通过您的Activity
中恢复您的保存状态。这两个方法onCreate()
和onRestoreInstanceState()
回调方法都会收到相同的Bundle
包含实例状态信息。唯一不一样的是,onCreate
中的savedInstanceState
可能是空的,只有由于系统约束重新创建时才不为空。而onRestoreInstanceState
中的savedInstanceState
一定不为空。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // 记得总是调用父类
// 检查是否正在重新创建一个以前销毁的实例
if (savedInstanceState != null) {
// 从已保存状态恢复成员的值
value = savedInstanceState.getString("KEY");
}
}
当然,你也可以不用onCreate
去恢复数据,使用onRestoreInstanceState
可以省去null
判断。
public void onRestoreInstanceState(Bundle savedInstanceState) {
// 总是调用超类,以便它可以恢复视图层次超级
super.onRestoreInstanceState(savedInstanceState);
// 从已保存的实例中恢复状态成员
value = savedInstanceState.getString("KEY");
}
Android
系统提供了很多丰富的API
去实现UI
的2D
与3D
动画,最主要的划分可以分为如下几类:
View
的动画。Frame
动画、帧动画)其实可以划分到视图动画的类别,专门用来一个一个的显示Drawable
的resources
,就像放幻灯片一样。Android 3.0(API 11)
以上版本系统才有效,这种动画可以设置给任何Object
,包括那些还没有渲染到屏幕上的对象。这种动画是可扩展的,可以自定义任何类型和属性的动画。视图动画(View Animation
)又称补间动画(Tween Animation
),即给出两个关键帧通过一些算法将给定属性值在给定的时间内在两个关键帧间渐变。视图动画只能作用于View
对象,是对View
的变换,而且View
的位置并未更改,很容易导致其点击事件响应错位。
在Android3.0(api 11)
之前,是不能用属性动画的,只能用补间动画,而补间动画所做的动画效果只是将View
的显示转为图片
,然后再针对这个图片做透明度、平移、旋转、缩放等效果。这带来的问题是:View所在的区域并没有发生变化,变化的只是个“幻影”而已。也就是说,在Android 3.0
之前,要想将View
区域发生变化,就得改变top
、left
、right
、bottom
。如果想让View
的动画是实际的位置发生变化,并且要兼容3.0
之前的软件,为了解决这个问题,从3.0
开始,加了几个新的参数:x
,y
,translationX
,translationY
。
x = left + translationX;
y = top + translationY;
这样,如果想要移动View
,只需改变translationX
和translationY
就可以了,top
和left
不会发生变化。也可以使用属性动画去改变translationX
和translationY
。
默认支持的类型有:
可以使用AnimationSet
让多个动画集合在一起运行,使用插值器(Interpolator
)设置动画的速度。上面的几种动画以及AnimationSet
都是Animation
的子类,因此Animation
中有的属性以及xml
的配置属性他们都有,对于使用xml
配置时需要放到res
下面的anim
文件夹下。
插值器有:
动画原理解析
动画就是根据间隔时间,不停的去刷新界面,把时间分片,在那个时间片,通过传入插值器的值到Animation.applyTransformation()
,来计算当前的值(比如旋转角度值,透明度等)。在动画的执行过程中,会反复的调用applyTransformation()
方法。每次调用参数interpolatedTime
值都会变化,该参数从0.0
递增为1.0
,当该参数为1.0
时表示动画结束。Transformation
类封装了矩阵Matrix
和透明度Alpha
值,通过参数Transformation
来设置Matrix
和Alpha
,实现各种效果。因此,可以继承Animation
,重写applyTransformation()
来实现其他的动画。
要使用View
动画,首先要创建XML文件,我们需要在res
下新建anim
文件夹,接着在anim
下创建view_anim.xml
:
<set
xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="0%p"
android:toXDelta="20%p"
android:fromYDelta="0%p"
android:toYDelta="20%p"
android:duration="4000"/>
<scale
android:fromXScale="1.0"
android:toXScale="0.2"
android:fromYScale="1.0"
android:toYScale="0.2"
android:pivotX="50%"
android:pivotY="50%"
android:duration="4000"/>
<rotate
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
android:duration="4000"/>
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.2"
android:duration="4000"/>
set>
上面的代码我们知道,View
动画既可以是单个动画,也可以有一系列动画组成。
这是因为Vie
w动画的四种种类分别对应着Animation
的四个子类(TranslateAnimation
,ScaleAnimation
,RotateAnimation
,AlphaAnimation
),除了以上四个子类它还有一个AnimationSet
类,对应xml
标签为
,它是一个容器,可以包含若干个动画,并且内部也可以继续嵌套
集合的。我们在activity
对TextView
设置动画:
Animation animation = AnimationUtils.loadAnimation(MainActivity.this,R.anim.view_anim);
textView.startAnimation(animation);
Drawable动画其实就是Frame动画(帧动画,也属于View
动画)。通过顺序播放一系列图像从而产生动画效果,可以简单理解为图片切换动画效果,但图片过多过大会导致OOM
。
<-- oneshot true代表只执行一次,false循环执行 -->
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false">
<item android:drawable="@mipmap/pic1" android:duration="200"/>
<item android:drawable="@mipmap/pic2" android:duration="200"/>
animation-list>
ImageView rocketImage = (ImageView) findViewById(R.id.rocket_image);
rocketImage.setBackgroundResource(R.drawable.rocket_thrust);
rocketAnimation = (AnimationDrawable) rocketImage.getBackground();
rocketAnimation.start();
补间动画由于动画效果局限性(平移、旋转、缩放、透明度)、View
位置无法改变特性以及只能作用于View
对象的缺陷,在 Android 3.0(API 11)
开始,系统提供了一种全新的动画模式:属性动画(Property Animation
)。它可以作用任意 Java
对象,可自定义各种动画效果。
工作原理:
属性动画通过在预设的规则下不断地改变值,并将值设置给作用对象的属性,从而达到动画效果。这个规则可以由我们定制,其中有两个重要的工具,可以帮助我们定制值的变化规则,一个是插值器(Interpolator
)提供变化速率,一个是估值器(TypeEvaluator
)提供变化的开始结束范围。
估值器:
估值器的作用是根据开始值、结束值、开始结束之间的一个比例来计算出最终的值。在属性动画上则是计算出动画对应的最终的属性值。系统默认提供了3
种估值器。
属性动画主要通过两个类来使用,ValueAnimator
和ObjectAnimator
,这两个的区别主要是ValueAnimator
只是计算值,赋值给对象属性需要我们手动监听值的变化来进行;ObjectAnimator
则对赋值属性这一步封装进了内部,也就是自动赋值。
ValueAnimator
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f); //估值器,变化范围从0到1
valueAnimator.setDuration(2000);//设置动画持续时间,以毫秒为单位
valueAnimator.setInterpolator(new DecelerateInterpolator());//设置插值器
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);//设置动画重复模式,RESTART表示动画正序重复,REVERSE代表倒序重复
valueAnimator.setRepeatCount(1);//设置重复次数,也就是动画播放次数=RepeatCount+1,ValueAnimator.INFINITE表示无限重复
valueAnimator.setStartDelay(1000);//设置动画延迟播放的时间
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {//动画值更新时的监听器
@Override
public void onAnimationUpdate(ValueAnimator animation) {//动画更新时会回调此方法
Float value= (Float) animation.getAnimatedValue();//拿到更新后动画值
}
});
ObjectAnimator
相比于ValueAnimator
,ObjectAnimator
可能才是我们最常接触到的类,因为ValueAnimator
只不过是对值进行了一个平滑的动画过渡,但我们实际使用到这种功能的场景好像并不多。而ObjectAnimator
则就不同了,它是可以直接对任意对象的任意属性进行动画操作的,比如说View
的alpha
属性。
ObjectAnimator
利用反射调用对象属性的set
方法从而自动赋值给对象属性完成动画,也就是说ObjectAnimator
作用的对象必须提供该属性的set
方法(如果没有提供初始值,还必须提供get
方法)需要注意的是,set
和get
方法名必须满足如下规则:set/get
+属性名(属性名头字母大写)。
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);
animator.setInterpolator(new DecelerateInterpolator());//设置插值器
animator.setDuration(5000);
animator.start();
组合动画(AnimatorSet)
独立的动画能够实现的视觉效果毕竟是相当有限的,因此将多个动画组合到一起播放就显得尤为重要。幸运的是,Android
团队在设计属性动画的时候也充分考虑到了组合动画的功能,因此提供了一套非常丰富的API
来让我们将多个动画组合到一起。
实现组合动画功能主要需要借助AnimatorSet
这个类,这个类提供了一个play()
方法,如果我们向这个方法中传入一个Animator
对象(ValueAnimator
或ObjectAnimator
)将会返回一个AnimatorSet.Builder
的实例,AnimatorSet.Builder
中包括以下四个方法:
好的,有了这四个方法,我们就可以完成组合动画的逻辑了,那么比如说我们想要让TextView
先从屏幕外移动进屏幕,然后开始旋转360
度,旋转的同时进行淡入淡出操作,就可以这样写:
ObjectAnimator moveIn = ObjectAnimator.ofFloat(textview, "translationX", -500f, 0f);
ObjectAnimator rotate = ObjectAnimator.ofFloat(textview, "rotation", 0f, 360f);
ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);
AnimatorSet animSet = new AnimatorSet();
animSet.play(rotate).with(fadeInOut).after(moveIn);
animSet.setDuration(5000);
animSet.start();
class A {
static {
System.out.println("A的静态块");
}
private static String staticStr = getStaticStr();
private String str = getStr();
{
System.out.println("A的实例块");
}
public A() {
System.out.println("A的构造方法");
}
private static String getStaticStr() {
System.out.println("A的静态属性初始化");
return null;
}
private String getStr() {
System.out.println("A的实例属性初始化");
return null;
}
public static void main(String[] args) {
new B();
new B();
}
}
class B extends A{
private static String staticStr = getStaticStr();
static {
System.out.println("B的静态块");
}
{
System.out.println("B的实例块");
}
public B() {
System.out.println("B的构造方法");
}
private String str = getStr();
private static String getStaticStr() {
System.out.println("B的静态属性初始化");
return null;
}
private String getStr() {
System.out.println("B的实例属性初始化");
return null;
}
}
实例化子类的时候,若此类未被加载过,首先加载是父类的类对象,然后加载子类的类对象,接着实例化父类,最后实例化子类,若此类被加载过,不再加载父类和子类的类对象。
接下来是加载顺序,当加载类对象时,首先初始化静态属性,然后执行静态块;当实例化对象时,首先执行构造块(直接写在类中的代码块),然后执行构造方法。至于各静态块和静态属性初始化哪个些执行,是按代码的先后顺序。属性、构造块(也就是上面的实例块)、构造方法之间的执行顺序(但构造块一定会在构造方法前执行),也是按代码的先后顺序。
JNI
的全称是Java Native Interface
(Java
本地接口)是一层接口,是用来沟通Java
代码和C/C++
代码的,是Java
和C/C++
之间的桥梁。通过JNI
,Java
可以完成对外部C/C++
库函数的调用,相对的,外部C/C++
也能调用Java
中封装好的类和方法。
Java
的优点是跨平台,和操作系统之间的调用由JVM
完成,但是一些和操作系统相关的操作就无法完成,JNI
的出现刚好弥补了这个缺陷,也完善了Java
语言,将Java
扩展得更为强大。
开发
JNI
需要NDK
的支持,NDK
(Native Development Kit
)是Android
所提供的一个工具集合,通过NDK
可以在Android
中更加方便地通过JNI
来调用本地代码(C/C++
)。NDK
提供了交叉编译器,开发时只需要修改.mk
文件就能生成特定的CPU
平台的动态库。
C/C++
开发的,通过JNI
,Java
可以调用C/C++
实现的驱动,从而扩展Java
虚拟机的能力。C
开发的。File->Setting->Tools->External Tools->Add External Tools:
Program: javah
Arguments: -v -jni -d $ModuleFileDir$/src/main/jni $FileClass$
Working directory: $SourcepathEntry$
生成JNI头文件:
#LOCAL_PATH是所编译的C文件的根目录,右边的赋值代表根目录即为Android.mk所在的目录
LOCAL_PATH:=$(call my-dir)
#在使用NDK编译工具时对编译环境中所用到的全局变量清零
include $(CLEAR_VARS)
#最后声称库时的名字的一部分
LOCAL_MODULE:=JniTest
#要被编译的C文件的文件名
LOCAL_SRC_FILES:=JniTest.c
#NDK编译时会生成一些共享库
include $(BUILD_SHARED_LIBRARY)
#表示编译选择的平台
#APP_ABI := armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64
#表示编译全部平台
APP_ABI := all
android {
...
externalNativeBuild {
ndkBuild {
path file('src/main/jni/Android.mk')
}
}
}
Log.e("yhd-",new JniTest().sayHello());
JNIEnv
代表 Java
运行环境, 可以使用 JNIEnv
调用 Java
中的代码;Java
对象传入 JNI
层就是 Jobject
对象, 需要使用 JNIEnv
来操作这个 Java
对象;Windows
中,定义为__declspec(dllexport)
。因为Windows
编译 dll
动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加此标识,表示将该函数导出在外部可以调用。Windows
中定义为:_stdcall
,一种函数调用约定。静态注册
上述方式就是注册
动态注册
我们知道Java Native
函数和JNI
函数时一一对应的,JNI
中就有一个叫JNINativeMethod
的结构体来保存这个对应关系,实现动态注册方就需要用到这个结构体。
1.声明native方法还是一样的
public class JavaHello {
public static native String hello();
}
2.创建jni目录,然后在该目录创建hello.c文件,如下:
#include
#include
#include
#include
#include
/**
* 定义native方法
*/
JNIEXPORT jstring JNICALL native_hello(JNIEnv *env, jclass clazz)
{
printf("hello in c native code./n");
return (*env)->NewStringUTF(env, "hello world returned.");
}
// 指定要注册的类
#define JNIREG_CLASS "com/yhd/JavaHello"
// 定义一个JNINativeMethod数组,其中的成员就是Java代码中对应的native方法
static JNINativeMethod gMethods[] = {
{ "hello", "()Ljava/lang/String;", (void*)native_hello},
};
static int registerNativeMethods(JNIEnv* env, const char* className,
JNINativeMethod* gMethods, int numMethods) {
jclass clazz;
clazz = (*env)->FindClass(env, className);
if (clazz == NULL) {
return JNI_FALSE;
}
if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
/***
* 注册native方法
*/
static int registerNatives(JNIEnv* env) {
if (!registerNativeMethods(env, JNIREG_CLASS, gMethods, sizeof(gMethods) / sizeof(gMethods[0]))) {
return JNI_FALSE;
}
return JNI_TRUE;
}
/**
* 如果要实现动态注册,这个方法一定要实现
* 动态注册工作在这里进行
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;
if ((*vm)-> GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
assert(env != NULL);
if (!registerNatives(env)) { //注册
return -1;
}
result = JNI_VERSION_1_4;
return result;
}
先仔细看一下上面的代码,看起来好像多了一些代码,稍微解释下,如果要实现动态注册就必须实现JNI_OnLoad
方法,这个是JNI
的一个入口函数,我们在Java
层通过System.loadLibrary
加载完动态库后,紧接着就会去查找一个叫JNI_OnLoad
的方法。如果有,就会调用它,而动态注册的工作就是在这里完成的。在这里我们会去拿到JNI中一个很重要的结构体JNIEnv
,env
指向的就是这个结构体,通过env指针可以找到指定类名的类,并且调用JNIEnv
的RegisterNatives
方法来完成注册native
方法和JNI
函数的对应关系。
()Ljava/lang/String:方法签名,对应规则如下:
静态注册
动态注册
Android开发中调用一个类中没有公开的方法,可以进行反射调用,而JNI开发中C调用java的方法也是反射调用。
//TextJNI -> public int add(int a, int b){return a+b;}
JNIEXPORT void JNICALLJava_com_yhd_TestJNI_callbackIntmethod(JNIEnv *env, jobject object) {
//获取字节码对
jclass clzz=(*env)->FindClass(env,"com/yhd/TextJNI");
//通过字节码对象找到方法对象,后面的参数是方法签名信息,参考上面提到的
jmethodID methodID=(*env)->GetMethodID(env,clzz,"add","(II)I");
//通过对象调用方法,可以调用空参数方法,也可以调用有参数方法,并且将参数通过调用的方法传入
int result=(*env)->CallIntMethod(env,object,methodID,3,4);
整个过程大概要经过如下:
下面提供JNI源码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define CHIP_ADDR 0xa0 //mcu i2c addr
#define I2C_DEV "/dev/i2c-1" // register i2c B bus
#define LOG_TAG "i2c" //android logcat
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
JNIEXPORT jint JNICALL Java_com_yhd_open(JNIEnv *env, jobject obj, jstring file){
char fileName[64];
const jbyte *str;
str = (*env)->GetStringUTFChars(env, file, NULL);
if (str == NULL) {//Can't get file name!
return -1;
}
(*env)->ReleaseStringUTFChars(env, file, str);
return open(fileName, O_RDWR);
}
JNIEXPORT jint JNICALL Java_com_yhd_read(JNIEnv * env, jobject obj, jint fileHander, jint slaveAddr, jintArray bufArr, jint len){
jint *bufInt;
char *bufByte;
int res = 0, i = 0, j = 0;
if (len <= 0) {//I2C: buf len <=0
goto err0;
}
bufInt = (jint *) malloc(len * sizeof(int));
if (bufInt == 0) {//I2C: nomem
goto err0;
}
bufByte = (char*) malloc(len);
if (bufByte == 0) {//I2C: nomem
goto err1;
}
(*env)->GetIntArrayRegion(env, bufArr, 0, len, bufInt);
memset(bufByte, '\0', len);
if ((j = read(fileHander, bufByte, len)) != len) {
LOGE("read fail in i2c read jni i = %d buf 4", i);
goto err2;
} else {
for (i = 0; i < j ; i++)
bufInt[i] = bufByte[i];
LOGI("return %d %d %d %d in i2c read jni", bufByte[0], bufByte[1], bufByte[2], bufByte[3]);
(*env)->SetIntArrayRegion(env, bufArr, 0, len, bufInt);
}
free(bufByte);
free(bufInt);
return j;
err2:
free(bufByte);
err1:
free(bufInt);
err0:
return -1;
}
JNIEXPORT jint JNICALL Java_com_yhd_write(JNIEnv *env, jobject obj, jint fileHander, jint slaveAddr, jint mode, jintArray bufArr, jint len){
#if 0
jint *bufInt;
char *bufByte;
int res = 0, i = 0, j = 0;
if (len <= 0) {//I2C: buf len <=0
goto err0;
}
bufInt = (jint *) malloc(len * sizeof(int));
if (bufInt == 0) {//I2C: nomem
goto err0;
}
bufByte = (char*) malloc(len + 1);
if (bufByte == 0) {//I2C: nomem
goto err1;
}
(*env)->GetIntArrayRegion(env, bufArr, 0, len, bufInt);
bufByte[0] = mode;
for (i = 0; i < len; i++)
bufByte[i + 1] = bufInt[i];
if ((j = write(fileHander, bufByte, len + 1)) != len + 1) {//write fail in i2c
goto err2;
}
free(bufByte);
free(bufInt);
return j - 1;
err2:
free(bufByte);
err1:
free(bufInt);
err0:
return -1;
#endif
}
JNIEXPORT void JNICALL Java_com_yhd_close(JNIEnv *env, jobject obj, jint fileHander){
close(fileHander);
}
Android
也采用分层的架构设计,从高到低分别是系统应用层(System Apps
),Java API
框架层(Java API Framework
),Android
系统运行层(包括Android Runtime
和原生态的C/C++
库 Native C/C++ Libraries
)、硬件抽象层(Hardware Abstraction Layer
)、Linux
内核层(Linux Kernel
)。如下图所示:
1、Linux内核层
Android
是基于Linux
内核的(Linux
内核提供了安全性、内存管理、进程管理、网络协议和驱动模型等核心系统服务),Linux
内核层为各种硬件提供了驱动程序,如显示驱动、相机驱动、蓝牙驱动、电池管理等等。
2、硬件抽象层(Hardware Abstraction Layer)
这是从软件设计角度看:主要目的在于将硬件抽象化。硬件抽象化可以隐藏特定平台的硬件接口细节,为上面一层提供固定统一的接口,使其具有硬件无关性。
从版权/保护厂家利益因素角度看:Android
基于Linux
内核实现,Linux
是GPL
许可,即对源码的修改都必须开源,而Android
是ASL
许可,即可以随意使用源码,无需开源,因此将原本位于kernel
的硬件驱动逻辑转移到Android
平台来,就可以不必开源,从而保护了厂家的利益。因此Android
就提供了一套访问硬件抽象层动态库的接口,各厂商在Android
的硬件抽象层实现特定硬件的操作细节,并编译成so
库,以库的形式提供给用户使用。(Android
是一个开放的平台,并不是一个开源的平台。)
3、Android系统运行层
原生态的C/C++库:通过C
或者C++
库为Android
系统提供主要的特性支持,例如Surface Manager管理访问显示子系统和从多模块应用中无缝整合2D
和3D
的图形,WebKit
提供了浏览器支持等。可以使用 Android NDK
直接从访问某些原生态库。
Android Runtime:Android
运行时,其中包括了ART
虚拟机(Android 5.0
之前是Dalvik
虚拟机,ART
模式与Dalvik
模式最大的不同在于,在启用ART
模式后,系统在安装应用的时候会进行一次预编译,在安装应用程序时会先将代码转换为机器语言存储在本地,这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。如果您的应用在 ART
上运行效果很好,那么它应该也可在 Dalvik
上运行,但反过来不一定。),每个Java
程序都运行在ART
虚拟机上,该虚拟机专门针对移动设备进行了定制,每个应用都有其自己的 Android Runtime (ART)
实例。此外,Android
运行时还包含一套核心运行时库,可提供 Java API
框架使用的 Java
编程语言大部分功能,包括一些 Java 8
语言功能。
4、Java API 框架层
这一层主要提供了构建应用程序时可能用到的各种API
,开发者通过这一层的API构建自己的APP
,这一层也是APP
开发人员必须要掌握的内容。其中最重要的是AMS
、WMS
、PMS
。
5、系统应用层
系统内置的应用程序以及非系统级的应用程序都属于应用层,负责与用户进行直接交互,通常都是用Java进行开发的。
当用户按下开机键的时候,引导芯片加载BootLoader到内存中,开始拉起Linux OS,一旦Linux内核启动完毕后,它就会在系统文件中寻找 init.rc 文件,并启动init 进程。
1、Init
在启动的init进程中,会进入system/core/init/init.cpp
文件的main
方法中。做了一以下事情:
init.rc
配置文件,并且启动了Zygote
进程。2、Zygote
在Android
系统中,DVM
(Dalvik
虚拟机)和ART
、应用程序进程以及运行系统关键服务的SystemServer
进程都是由Zygote
进程创建的,我们也将它称为孵化器(本来字面意思就是受精卵)。它通过fork
复制进程的形势来创建应用进程和SystemServer
进程,由于Zygote
进程在启动时会创建DVM
或者ART
,因此通过fork
而创建的应用程序进程和SystemServer
进程可以在内部获取一个DVM
或者ART的实例副本。Zygote
进程启动公做了几件事:
AndroidRuntime
并调用其start
方法,启动Zygote
进程。Java
虚拟机并为Java
虚拟机注册JNI
方法。ZygoteInit
的main
函数进入Zygote
的Java
框架层。registerZygoteSocket
方法创建服务端Socket
,并通过runSelectLoop
方法等待AMS
的请求来创建新的应用程序进程。SystemServer
。3、SystemServer
SystemServer
在启动后,陆续启动了各项服务,包括ActivityManagerService
,PowerManagerService
,PackageManagerService
等等,而这些服务的父类都是SystemService
。总结一下SystemServer
进程:
Binder
线程池。SystemServiceManager
(用于对系统服务进行创建、启动和生命周期管理)。Android
的Framework
是直接应用之下的一层,叫做应用程序框架层。这一层是核心应用程序所使用的API
框架,为应用层提供各种API
,提供各种组件和服务来支持我们的Android
开发,包括ActivityManager
、WindowManager
、ViewSystem
等。我们可以称Framework
层才真正是Java语言实现的层,在这层里定义的API都是用Java
语言编写,但是又因为它包含了JNI
的方法,JNI
用C/C++
编写接口,根据函数表查询调用核心库层里的底层方法,最终访问到Linux
内核。那么Framework
层的作用就有2
个:
Java
语言编写一些规范化的模块封装成框架,供APP
层开发者调用开发出具有特殊业务的手机应用。Java Native Interface
调用core lib
层的本地方法,JNI
的库是在Dalvik
虚拟机启动时加载进去的,Dalvik
会直接去寻址这个JNI
方法,然后去调用。内存管理,进程管理,管理着Activity
的任务堆栈、Service
的启动、ContentProvider
启动、BroadCast Receiver
列表管理等。AMS
也决定了某个进程会不会被杀死。AMS
实现了IBinder
接口,所以它是一个Binder
,这意味着它不但可以用于进程间通信,还是一个线程,因为一个Binder
就是一个线程。这个怎么理解呢?可以这么理解,如果我们启动一个Hello World安卓应用程序,里面不另外启动其他线程,这个进程里面我们最起码启动了4个线程:
Activity
的onCreate()
方法,一般就是这个main
线程来执行代码,也叫ui
线程。这是因为安卓的组件(包括TextView
, Button
等等这些ui
控件)都是非线程安全的,所以只允许main
线程来操作。java
有垃圾回收机制,每个java
程序(一个java
虚拟机)都有一个垃圾回收线程,专门回收不再使用的对象。IBinder
接口,用于进程间通信。具体来说,是我们的应用程序和Ams通信的工具。IBinder
接口,是用于我们的应用程序和Wms通信的工具。下面看看Activity如何与AMS通信的:
ActivityThread
ActivityThread
就是我们常说的主线程或UI
线程,ActivityThread
的main
方法是整个APP
的入口。当AMS
拉起一个新的进程,同时启动一个主线程的时候,主线程就从ActivityThread.main
方法开始执行,它会初始化一些对象,然后自己进入消息等待队列, 也就是Looper.loop()
。一旦进入loop()
方法,线程就进入了死循环,再也不会退出;一直在等待别人给他消息,然后执行这个消息。
//ActivityThread的main方法
public static void main(String[] args) {
...
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
//在attach方法中会完成Application对象的初始化,然后调用Application的onCreate()方法
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
...
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
ApplicationThread
ApplicationThread
是ActivityThread
的私有内部类,实现了IBinder
接口,用于ActivityThread
和ActivityManagerService
的所在进程间通信。
APP
图标时,Launcher
的startActivity()
方法,通过Binder
通信,调用system_server
进程中AMS
服务的startActivity
方法,发起启动请求。system_server
进程接收到请求后,向Zygote
进程发送创建进程的请求。Zygote
进程fork
出App
进程,并执行ActivityThread
的main
方法,创建ActivityThread
线程,初始化MainLooper
,主线程Handler
,同时初始化ApplicationThread
用于和AMS
通信交互。App
进程,通过Binder
向sytem_server
进程发起attachApplication
请求,这里实际上就是APP
进程通过Binder
调用sytem_server
进程中AMS
的attachApplication
方法,上面我们已经分析过,AMS
的attachApplication
方法的作用是将ApplicationThread
对象与AMS
绑定。system_server
进程在收到attachApplication
的请求,进行一些准备工作后,再通过binder
IPC
向App进程发送handleBindApplication
请求(初始化Application
并调用onCreate
方法)和scheduleLaunchActivity
请求(创建启动Activity
)。App
进程的binder
线程(ApplicationThread
)在收到请求后,通过handler
向主线程发送BIND_APPLICATION
和LAUNCH_ACTIVITY
消息,这里注意的是AMS
和主线程并不直接通信,而是AMS
和主线程的内部类ApplicationThread
通过Binder
通信,ApplicationThread
再和主线程通过Handler
消息交互。Message
后,创建Application
并调用onCreate
方法,再通过反射机制创建目标Activity
,并回调Activity.onCreate()
等方法。App
便正式启动,开始进入Activity
生命周期,执行完onCreate/onStart/onResume
方法,UI
渲染后显示APP主界面。冷启动(Cold start)
冷启动是指APP在手机启动后第一次运行,或者APP
进程被kill
掉后在再次启动。可见冷启动的必要条件是该APP进程不存在,这就意味着系统需要创建进程,APP
需要初始化。在这三种启动方式中,冷启动耗时最长,对于冷启动的优化也是最具挑战的。因此本文重点谈论的是对冷启动相关的优化。
温启动(Warm start)
App
进程存在,当时Activity
可能因为内存不足被回收。这时候启动App
不需要重新创建进程,但是Activity
的onCrate
还是需要重新执行的。场景类似打开淘宝逛了一圈然后切到微信去聊天去了,过了半小时再次回到淘宝。这时候淘宝的进程存在,但是Activity可能被回收,这时候只需要重新加载Activity
即可。
热启动(Hot start)
App
进程存在,并且Activity
对象仍然存在内存中没有被回收。可以重复避免对象初始化,布局解析绘制。场景就类似你打开微信聊了一会天这时候出去看了下日历在打开微信 微信这时候启动就属于热启动。
启动原理
点击桌面后的APP启动,通过startActivity
方式的启动。调用startActivity
,该方法经过层层调用,最终会调用ActivityStackSupervisor.java
中的startSpecificActivityLocked
,当activity
所属进程还没启动的情况下,则需要创建相应的进程,最终进程由 Zygote
Fork
进程。进程启动后立即显示应用程序的空白启动窗口。而一旦App
进程完成了第一次绘制,系统进程就会用MainActivity
替换已经展示的Background Window
,此时用户就可以使用App
了。
启动时间优化
这里涉及到:异步加载、延时加载、懒加载
App
启动时,空白的启动窗口将保留在屏幕上,直到系统首次完成绘制应用程序。此时,系统进程会交换应用程序的启动窗口,允许用户开始与应用程序进行交互。如果应用程序中重载了Application.onCreate()
,系统会调用onCreate()
方法。之后,应用程序会生成主线程(也称为UI
线程),并通过创建MainActivity
来执行任务。onCreate()
方法对加载时间的影响最大,因为它以最高的开销执行工作:加载并绘制视图,以及初始化Activity
运行所需的对象。用户体验优化
- true
:Activity.onCreate()
之前App
不做显示,这样用户误以为是手机慢了,这种瞒天过海的方案大家还是不要用了。
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/white" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/ic_github" />
item>
layer-list>
2、弄一个主题:<style name="SplashTheme" parent="AppTheme">
- "android:windowBackground"
>@drawable/logo_splash
style>
3、将一个什么不渲染布局的Activity作为启动屏,并加上主题:<activity
android:name=".ui.module.main.LogoSplashActivity"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
intent-filter>
activity>
其实Activity
的 onCreate/onStart/onResume
三个回调中,是在第一个performTraversals
执行的,并没有执行Measure
和Layout
操作,这个是在后面的performTraversals
中才执行的,所以在这之前宽高都是0。可以在队列中拿到图片的宽高:getWindow().getDecorView().post(runnable)
和Handle.postDelay(runnable,milsecond)
,第二种延迟时间不好把握,第一种是最好的。
Fragment
回调了onResume
方法却并没有进去前台可见,所以不能仅仅依靠onResume判断。有个方法专门判断Fragment
是否可见:getUserVisibleHint()
;
一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用 ThreadLocal
,通过 ThreadLocal
可以在不同的线程中维护一套数据的副本并且彼此互不干扰。
ThreadLocal
是一个线程内部的数据存储类,通过它可以在 指定的线程中 存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据。
private void threadLocal() {
ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<>();
mThreadLocal.set(true);
Log.d(TAG, "[Thread#main]threadLocal=" + mThreadLocal.get());//true
new Thread() {
@Override
public void run() {
super.run();
mThreadLocal.set(false);
Log.d(TAG, "[Thread#1]threadLocal=" + mThreadLocal.get());//false
}
}.start();
new Thread() {
@Override
public void run() {
super.run();
Log.d(TAG, "[Thread#2]threadLocal=" + mThreadLocal.get());//null
}
}.start();
}
Android
的UI
访问是没有加锁的,多个线程可以同时访问更新操作同一个UI
控件。也就是说访问UI
的时候Android
系统当中的控件都不是线程安全的,这将导致在多线程模式下,当多个线程共同访问更新操作同一个UI
控件时容易发生不可控的错误,而这是致命的。所以Android
中规定只能在UI
线程中访问UI
,这相当于从另一个角度给Android
的UI
访问加上锁,一个伪锁。
题外话:我们在onCreate中创建线程更新UI,却能成功。
当访问UI
时,ViewRootImpl
会调用checkThread
方法检查当前访问UI
的线程是哪个,如果不是UI
线程则会抛出异常。ViewRootImpl
的创建是在onResume
方法回调之后,而我们一开篇是在onCreate
方法中创建子线程并访问UI,在那个时刻,ViewRootImpl
还没有来得及创建,无法检测当前线程是否是UI
线程,所以程序没有崩溃。而之后修改了程序,让线程休眠了100
毫秒后再更新UI
,程序就崩了。很明显在这100
毫秒内ViewRootImpl
已经完成了创建,并能执行checkThread
方法检查当前访问并更新UI
的线程是不是UI
线程。
Android UI
是线程不安全的,如果在子线程中尝试进行UI操作,程序就有可能会崩溃,因为在ViewRootImpl.checkThread
对UI
操作做了验证,导致必须在主线程中访问UI,但Android
在主线程中进行耗时的操作会导致ANR
,为了解决子线程无法访问UI
的矛盾,提供了消息机制。
Handler
通过sendMessage
或者post
发送消息,最终都调用sendMessageAtTime
将Message
交给MessageQueue
,MessageQueue.enqueueMessage
方法将Message以链表的形式放入队列中(同步操作,线程安全),Looper
的loop
方法循环调用MessageQueue.next()
取出消息,并且调用Handler
的dispatchMessage
来处理消息。在dispatchMessage
中,分别判断msg.callback(runnable)
不为空就执行回调,如果为空就执行handleMessage
。
Handler工作原理
Handler
是Android
给我们提供用来更新UI
的一套机制,是一套消息处理机制,可以通过它来发送消息和处理消息。消息发送通过post
系列方法和send
系列方法来实现,而post
最终还是调用sendMessageAtTime
方法来实现发送消息。Handler
的运行需要底层的MessageQueue
和Looper
的支撑。MessageQueue
即消息队列,存储消息的单元,但并不能处理消息,这时需要Looper
,它会无限循环查找是否有新消息,有即处理消息,没有就等待。
private Handler handler1;
private Handler handler2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler1 = new Handler();
new Thread(new Runnable() {
@Override
public void run() {
handler2 = new Handler();
}
}).start();
}
运行下会发现
handler2
会报下面的错误“Can't create handler inside thread that has not called Looper.prepare()
”
为什么handler1
没有报错呢?因为Handler
的创建时会采用当前线程的Looper
来构建内部的消息循环系统,而handler1
是在主线程创建的,而主线程已经默认调用Looper.prepareMainLooper()
创建Looper
以及Looper.loop()
阻塞接受消息,所以handler2
创建时需要先调用Looper.prepare()
创建Looper
,最后执行Looper.loop()
开始运作。
MessageQueue工作原理
MessageQueue
只有两个操作:插入和读取。其内部是一个单链表的数据结构来维护消息列表,链表的节点就是 Message
。它提供了 enqueueMessage()
(内部用了synchronized
同步锁保证线程安全)来进行插入新的消息,提供next()
从链表中取出消息,值得注意的是next()
会循环地从链表中取出 Message
交给 Handler
,但如果链表为空的话会阻塞这个方法,直到有新消息到来。
Looper工作原理
Looper
最重要的方法是loop
方法,只有调用了loop
后,消息系统才会真正的起作用。loop()
方法是一个死循环,唯一跳出就是next
返回null
。既然是死循环,为什么主线程没有卡死呢?
主线程的MessageQueue
没有消息时,便阻塞在loop
的queue.next()
中的nativePollOnce()
方法里,没有输入事件,Looper
此时处于空闲状态,此时主线程会释放CPU
资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe
管道写端写入数据来唤醒主线程工作。而真正的卡死,指的是有输入事件的,MessageQueue
是不为空的,那么Looper
依然会正常轮询。线程也没有阻塞。当事件的执行时间过长的话,而且此时与其他事件都没办法处理,那么就是真正的卡死了,然后就ANR
了。
通过startService
启动后,Service
会一直无限期运行下去,只有外部调用了stopService()
或stopSelf()
方法时,该Service
才会停止运行并销毁。
pulic class TestOneService extends Service{
@Override
public void onCreate() {
super.onCreate();
//onCreate()只会在第一次创建service时候调用,多次执行startService()不会重复调用onCreate(),此方法适合完成一些初始化工作。
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
//如果多次执行了Context的startService()方法,那么Service的onStartCommand()方法也会相应的多次调用。onStartCommand()方法很重要,我们在该方法中根据传入的Intent参数进行实际的操作,比如会在此处创建一个线程用于下载数据或播放音乐等。
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
//Service中的onBind()方法是抽象方法,Service类本身就是抽象类,所以onBind()方法是必须重写的,即使我们用不到。
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
//在销毁的时候会执行Service该方法。
}
}
bindService启动服务特点:
bindService
启动的服务和调用者之间是典型的client
-server
模式。调用者是client
,service
则是server
端。service
只有一个,但绑定到service
上面的client
可以有一个或很多个。这里所提到的client
指的是组件,比如某个Activity
。client
可以通过IBinder
接口获取Service
实例,从而实现在client
端直接调用Service
中的方法以实现灵活交互,这在通过startService
方法启动中是无法实现的。bindService
启动服务的生命周期与其绑定的client
息息相关。当client
销毁时,client
会自动与Service
解除绑定。当然,client
也可以明确调用Context
的unbindService()
方法与Service
解除绑定。当没有任何client
与Service
绑定时,Service
会自行销毁。public class TestTwoService extends Service{
//client 可以通过Binder获取Service实例
public class MyBinder extends Binder {
public TestTwoService getService() {
return TestTwoService.this;
}
}
//通过binder实现调用者client与Service之间的通信
private MyBinder binder = new MyBinder();
@Nullable
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public boolean onUnbind(Intent intent) {
return false;
}
//其他生命周期省略
}
绑定与解绑
private ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
isBind = true;
TestTwoService.MyBinder myBinder = (TestTwoService.MyBinder) binder;
service = myBinder.getService();
}
@Override
public void onServiceDisconnected(ComponentName name) {
isBind = false;
}
};
//绑定
bindService(intent, conn, BIND_AUTO_CREATE);
//解绑
unbindService(conn);
CPU
调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一些在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。1、Bundle
在Android
中三大组件(Activity
,Service
,Receiver
)都支持在Intent
中传递Bundle
数据,由于Bundle
实现了Parcelable
接口(一种特有的序列化方法),所以它可以很方便的在不同的进程之间进行传输。当在一个进程中启动另外一个进程的Activity
,Service
,Receiver
时,可以在Bundle
中附加需要传输给远程的进程的信息,并通过Intent
发送出去。
2、使用文件共享的方式
文件共享:将对象序列化之后保存到文件中,在通过反序列,将对象从文件中读取出来。此方式对文件的格式没有具体的要求,可以是文件、XML
、JSON
等。
文件共享方式也存在着很大的局限性,如并发读/写问题,如读取的数据不完整或者读取的数据不是最新的。文件共享适合在对数据同步要求不高的进程间通信,并且要妥善处理并发读/写的问题。
3、使用Messenger的方式
我们也可以通过Messenger
来进行进程间通信,在Messenger
中放入我们需要传递的数据,实现进程间数据传递。Messenger
只能传递Message对象,Messenger
是一种轻量级的IPC
方案,它的底层实现是AIDL
。
Messenger
内部消息处理使用Handler
实现的,所以它是以串行的方式处理客服端发送过来的消息的,如果有大量的消息发送给服务器端,服务器端只能一个一个处理,如果并发量大的话用Messenger
就不合适了,而且Messenger
的主要作用就是为了传递消息,很多时候我们需要跨进程调用服务器端的方法,这种需求Messenger
就无法做到了。
4、使用AIDL的方式
AIDL
(Android Interface Definition Language
)是一种IDL
语言,用于生成可以在Android
设备上两个进程之间进行进程间通信(IPC
)的代码。
如果在一个进程中(例如Activity
)要调用另一个进程中(例如Service
)对象的操作,就可以使用AIDL
生成可序列化的参数。AIDL
是IPC
的一个轻量级实现,Android
也提供了一个工具,可以自动创建Stub
(类架构,类骨架)。
只有当你允许来自不同的客户端访问你的服务并且需要处理多线程问题时你才必须使用AIDL
,其他情况下都可以选择其他方法。AIDL是处理多线程、多客户端并发访问的,而Messenger是单线程处理。
5、使用ContentProvider的方式
ContentProvider
(内容提供者)是Android
中的四大组件之一,为了在应用程序之间进行数据交换,Android
提供了ContentProvider
,ContentProvider
是不同应用之间进行数据交换的API
,一旦某个应用程序通过ContentProvider
暴露了自己的数据操作的接口,那么不管该应用程序是否启动,其他的应用程序都可以通过接口来操作接口内的数据,包括数据的增、删、改、查等操作。
6、使用广播接收者(Broadcast)的方式
广播是一种被动跨进程通信方式。当某个程序向系统发送广播时,其他的应用程序只能被动地接收广播数据。
Broadcast Receiver
本质上是一个系统级的监听器,它专门监听各个程序发出的Broadcast
,因此它拥有自己的进程,只要存在与之匹配的Intent
被广播出来,Broadcast Receiver
总会被激发。只要注册了某个广播之后,广播接收者才能收到该广播。广播注册的一个行为是将自己感兴趣的Intent Filter
注册到Android
系统的AMS
(Activity Manager Service
)中,里面保存了一个Intent Filter
列表。广播发送者将Intent Filter
的action
行为发送到AMS
中,然后遍历AMS
中的Intent Filter
列表,看谁订阅了该广播,然后将消息遍历发送到注册了相应的Intent Filter
或者Service
中。也就是说:会调用抽象方法onReceive()
方法。其中AMS
起到了中间桥梁的作用。
onReceiver()
方法中尽量不要做耗时操作,如果onReceiver()
方法不能再10
秒之内完成事件的处理,Android
会认为该进程无响应,也就弹出我们熟悉的ANR
。
7、使用Socket的方式
Socket
也是实现进程间通信的一种方式,Socket
也称为“套接字”(网络通信中概念),通过Socket
也可以实现跨进程通信,Socket
主要还是应用在网络通信中。
startForeground()方法
在onStartCommand
里面调用startForeground()
方法把Service
提升为前台进程级别,然后再onDestroy
里面要记得调用stopForeground ()
方法。也可以在onStartCommand()
方法中开启一个通知,提高进程的优先级。
onStartCommand方法,返回START_STICKY
在onStartCommand
方法中手动返回START_STICKY
,当service
因内存不足被kill
,当内存又存在的时候,service
又被重新创建,但是不能保证任何情况下都被重建,比如:进程被干掉了。
在onDestroy方法里发广播重启service
service+broadcast
方式,就是当service
走onDestory
的时候,发送一个自定义的广播,当收到广播的时候,重新启动service
。因为广播是AMS
级别的,所以可以保证应用退出还能收到广播。(第三方应用或是在setting
里-应用-强制停止时,APP
进程就直接被干掉了,onDestroy
方法都进不来,所以无法保证会执行)。
监听(系统)广播判断Service状态
通过监听系统、QQ
,微信,系统应用,友盟推送等的一些广播,比如:手机重启、界面唤醒、应用状态改变等监听并捕获到,然后判断我们的Service
是否还存活,并唤醒自己的App
。这是大厂常用方法,例如:阿里收购友盟后,通过友盟的广播唤醒阿里的App
,然后把自己启动了。
Application加上Persistent属性
这个是题外话,因为只有系统级别编译才有
android:persistent="true"
属性。
看Android的文档知道,当进程长期不活动,或系统需要资源时,会自动清理门户,杀死一些Service,和不可见的Activity等所在的进程。但是如果某个进程不想被杀死(如数据缓存进程,或状态监控进程,或远程服务进程),应该怎么做,才能使进程不被杀死。
监听锁屏方法
QQ
采取在锁屏的时候启动一个1
个像素的Activity
,当用户解锁以后将这个Activity
结束掉(顺便同时把自己的核心服务再开启一次)。
双进程守护
一个进程被杀死,另外一个进程又被他启动。相互监听启动。A<—>B
,杀进程是一个一个杀的。本质是和杀进程时间赛跑。在Android 5.0
前是有效的,5.0
之后就不行了。
priority
必须是整数,默认是0
, 范围是[-1000
, 1000
]intent
的filter
的类型。activity
和 receiver
是有意义的。activity
满足响应 的条件, 系统只会触发 priority
高的那个activity
。receiver
满足响应的条件,系统会优先触发priority
高的那个receiver
。<service
android:name="com.yhd.UploadService"
android:enabled="true" >
<intent-filter android:priority="1000" >
<action android:name="com.yhd.myservice" />
intent-filter>
service>
数据类型 | 占用字节数 | 位数 | 取值范围 |
---|---|---|---|
整数型:byte | 1 | 8 | -2的7次方到2的7次方-1 |
布尔型:boolean | 1 | 8 | -2的7次方到2的7次方-1 |
整数型:short | 2 | 16 | -2的15次方到2的15次方-1 |
字符型:char | 2 | 16 | -2的15次方到2的15次方-1 |
整数型:int | 4 | 32 | -2的31次方到2的31次方-1 |
浮点型:float | 4 | 32 | -2的31次方到2的31次方-1 |
整数型:long | 8 | 64 | -2的63次方到2的63次方-1 |
浮点型:double | 8 | 64 | -2的63次方到2的63次方-11 |
在Android
中 ,Dalvik
的垃圾回收算法为:标注与清理(Mark and Sweep
)和拷贝GC
,但是具体使用什么算法是在编译期决定的,无法在运行的时候动态更换。在/dalvik/vm/Dvm.mk
里,指明了"WITH_COPYING_GC
“选项,则编译”/dalvik/vm/alloc/Copying.cpp
“源码 ,此是Android
中拷贝GC算法
的实现,否则编译”/dalvik/vm/alloc/HeapSource.cpp
",使用标注与清理算法。到了Android 5.0
的ART
,有多个不同的 GC
方案,这些方案包括运行不同垃圾回收器。默认方案是 CMS
(并发标记清除)方案,下面详解。
也就是说,当垃圾回收开始清理资源时,其余的所有线程都会被停止。所以,我们要做的就是尽可能的让它执行的时间变短。如果清理的时间过长,在我们的应用程序中就能感觉到明显的卡顿。
GC
会被调用。因为GC
在优先级最低的线程中进行,所以当应用忙时,GC
线程就不会被调用,但以下条件除外。Java
堆内存不足时,GC
会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM
就会强制地调用GC
线程,以便回收内存用于新的分配。若GC
一次之后仍不能满足内存分配的要求,JVM
会再进行两次GC
作进一步的尝试,若仍无法满足要求,则 JVM
将报“OutOfMemory
”的错误,Java
应用将停止。对象什么时候回收?判断对象是否回收主要有以下两种算法。
引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1
;当引用失效时,计数器值就减1
;任何时刻计数器为0
的对象就是不可能再被使用的。
Objective-c
和swift
用的就是这种算法。strong/weak
类型指针或者retain/assign
类型指针分别修饰循环引用对象,破坏循环链。可达性分析算法(根搜索算法)
为了解决上面的循环引用问题,Java
采用了一种新的算法:可达性分析算法。
从GC Roots
作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之视为不可达。如果出现循环引用了,只要没有被GC Roots
引用了就会被回收,完美解决!
Java定义的GC Roots对象:
1、虚拟机栈(帧栈中的本地变量表)中引用的对象。
2、方法区中静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法栈中JNI引用的对象。
方法区回收
上面说的都是对堆内存中对象的判断,方法区中主要回收的是废弃的常量和无用的类。判断常量是否废弃可以判断是否有地方引用这个常量,如果没有引用则为废弃的常量,需要同时满足如下条件:
知道了什么时候回收对象,那我们再看具体怎么垃圾回收。
标记清除法
标记-清除法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
复制算法
复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
标记整理算法
该算法标记阶段和标记清除法一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
分代回收算法
分代回收算法其实不算一种新的算法,而是根据复制算法和标记整理算法的使用场景综合使用。根据对象存活的生命周期将内存划分为若干个不同的区域。
Eden:Survivor
为8:1
)1、首先,所有新生成的对象都是放在年轻代的Eden
分区的,初始状态下两个Survivor
分区都是空的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
2、当Eden
区满的的时候,小垃圾收集就会被触发。
3、当Eden
分区进行清理的时候,会把引用对象移动到第一个Survivor
分区,无引用的对象删除。
4、在下一个小垃圾收集的时候,在Eden
分区中会发生同样的事情:无引用的对象被删除,引用对象被移动到另外一个Survivor
分区(S1
)。此外,从上次小垃圾收集过程中第一个Survivor
分区(S0
)移动过来的对象年龄增加,然后被移动到S1
。当所有的幸存对象移动到S1
以后,S0
和Eden
区都会被清理。注意到,此时的Survivor
分区存储有不同年龄的对象。
5、在下一个小垃圾收集,同样的过程反复进行。然而,此时Survivor
分区的角色发生了互换,引用对象被移动到S0
,幸存对象年龄增大。Eden
和S1
被清理。
6、这幅图展示了从年轻代到老年代的提升。当进行一个小垃圾收集之后,如果此时年老对象此时到达了某一个个年龄阈值(例子中使用的是8
),JVM会把他们从年轻代提升到老年代。
7、随着小垃圾收集的持续进行,对象将会被持续提升到老年代。
8、这样几乎涵盖了年轻一代的整个过程。最终,在老年代将会进行大垃圾收集,这种收集方式会清理-压缩老年代空间。
也就是说,刚开始会先在新生代内部反复的清理,顽强不死的移到老生代清理,最后都清不出空间,就爆炸了。
参数 | 描述 |
---|---|
-Xms | JVM启动的时候设置初始堆的大小 |
-Xmx | 设置最大堆的大小 |
-Xmn | 设置年轻代的大小 |
-XX:PermSize | 设置持久代的初始的大小 |
-XX:MaxPermSize | 设置持久代的最大值 |
Java
虚拟机在运行时,会把内存空间分为若干个区域,根据《Java虚拟机规范(Java SE 7 版)》的规定,Java
虚拟机所管理的内存区域分为如下部分:方法区、堆内存、虚拟机栈、本地方法栈、程序计数器。
1、方法区
方法区主要用于存储虚拟机加载的类信息
、常量
、静态变量
,以及编译器编译后的代码
等数据。在jdk1.7
及其之前,方法区是堆的一个“逻辑部分”(一片连续的堆空间),但为了与堆做区分,方法区还有个名字叫“非堆”,也有人用“永久代”(HotSpot
对方法区的实现方法)来表示方法区。
从jdk1.7
已经开始准备“去永久代”的规划,jdk1.7
的HotSpot
中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中,(常量池除字符串常量池还有class
常量池等),这里只是把字符串常量池移到堆内存中;在jdk1.8
中,方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace
)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory
)。
去永久代的原因有:
(1)字符串存在永久代中,容易出现性能问题和内存溢出。
(2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
(3)永久代会为 GC
带来不必要的复杂度,并且回收效率偏低。
2、堆内存
堆内存主要用于存放对象和数组,它是JVM
管理的内存中最大的一块区域,堆内存和方法区都被所有线程共享,在虚拟机启动时创建。在垃圾收集的层面上来看,由于现在收集器基本上都采用分代收集算法,因此堆还可以分为新生代(YoungGeneration
)和老年代(OldGeneration
),新生代还可以分为Eden
、From Survivor
、To Survivor
。
3、程序计数器
程序计数器是一块非常小的内存空间,可以看做是当前线程执行字节码的行号指示器,每个线程都有一个独立的程序计数器,因此程序计数器是线程私有的一块空间,此外,程序计数器是Java
虚拟机规定的唯一不会发生内存溢出的区域。
4、虚拟机栈
虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应Java
代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个Java
方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。
5、本地方法栈
本地方法栈与虚拟机栈的区别是,虚拟机栈执行的是Java
方法,本地方法栈执行的是本地方法(Native Method
),其他基本上一致,在HotSpot
中直接把本地方法栈和虚拟机栈合二为一,这里暂时不做过多叙述。
1、堆内存溢出
堆内存中主要存放对象、数组等,只要不断地创建这些对象,并且保证GC Roots
到对象之间有可达路径来避免垃圾收集回收机制清除这些对象,当这些对象所占空间超过最大堆容量时,就会产生OutOfMemoryError
的异常。
2、虚拟机栈/本地方法栈溢出
StackOverflowError
,简单理解就是虚拟机栈中的栈帧数量过多(一个线程嵌套调用的方法数量过多)时,就会抛出该异常。最常见的场景就是方法无限递归调用。OutOfMemoryError
。我们可以这样理解,虚拟机中可以供栈占用的空间≈可用物理内存 - 最大堆内存 - 最大方法区内存,比如一台机器内存为4G
,系统和其他应用占用2G
,虚拟机可用的物理内存为2G
,最大堆内存为1G
,最大方法区内存为512M
,那可供栈占有的内存大约就是512M
,假如我们设置每个线程栈的大小为1M
,那虚拟机中最多可以创建512
个线程,超过512
个线程再创建就没有空间可以给栈了,就报OutOfMemoryError
异常了。3、方法区溢出
前面说到,方法区主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据,所以方法区溢出的原因就是没有足够的内存来存放这些数据。
由于在jdk1.6
之前字符串常量池是存在于方法区中的,所以基于jdk1.6
之前的虚拟机,可以通过不断产生不一致的字符串(同时要保证和GC Roots
之间保证有可达路径)来模拟方法区的OutOfMemoryError
异常;但方法区还存储加载的类信息,所以基于jdk1.7
的虚拟机,可以通过动态不断创建大量的类来模拟方法区溢出。
4、本机直接内存溢出
本机直接内存(DirectMemory
)并不是虚拟机运行时数据区的一部分,也不是Java
虚拟机规范中定义的内存区域,但Java
中用到NIO
相关操作时(比如ByteBuffer
的allocteDirect
方法申请的是本机直接内存),也可能会出现内存溢出的异常。
编写的Java
代码需要经过编译器编译为class
文件(从本地机器码转变为字节码的过程),class
文件是一组以8
位字节为基础的二进制流,这些二进制流分别以一定形式表示着魔数(用于标识是否是一个能被虚拟机接收的Class
文件)、版本号、字段表、访问标识等内容。代码编译为class
文件后,需要通过类加载器把class
文件加载到虚拟机中才能运行和使用。
类从被加载到内存到使用完成被卸载出内存,需要经历加载、连接、初始化、使用、卸载这几个过程,其中连接又可以细分为验证、准备、解析。
(1)加载
在加载阶段,虚拟机主要完成三件事情:
com.yhd.MyClassLoader
)来获取定义该类的二进制流;java.lang.Class
对象,作为程序访问方法区中这个类的外部接口。(2)验证
验证的目的是为了确保class文件的字节流包含的内容符合虚拟机的要求,且不会危害虚拟机的安全。
class
文件中二进制字节流的格式,比如魔数是否已0xCAFEBABY
开头、版本号是否正确等。Java
语言规范,比如验证这个类是否有父类(java.lang.Object
除外),如果这个类不是抽象类,是否实现了父类或接口中没有实现的方法等等。java.lang.NoSuchFieldError
”、“java.lang.NoSuchMethodError
”等异常。(3)准备
正式为类变量分配内存并设置类变量初始值,这些变量所使用的内存都分配在方法区。注意分配内存的对象是类变量而不是实例变量,而且为其分配的是“初始值”.一般数值类型的初始值都为0
,char
类型的初始值为’\u0000
’(常量池中一个表示Nul
的字符串),boolean
类型初始值为false
,引用类型初始值为null
。
(4)解析
解析是将常量池中符号引用替换为直接引用的过程。
符号引用是以一组符号来描述所引用的目标,符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。比如在com.yhd.LoggerFactory
类引用了com.yhd.Logger
,但在编译期间是不知道Logger类的内存地址的,所以只能先用com.yhd.Logger
(假设是这个,实际上是由类似于CONSTANT_Class_info
的常量来表示的)来表示Logger
类的地址,这就是符号引用。
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局有关,如果有了直接引用,那引用的目标一定在内存中存在。
(5)初始化
在准备阶段,已经为类变量赋了初始值,在初始化阶段,则根据程序员通过程序定制的主观计划去初始化类变量的和其他资源,也可以从另一个角度来理解:初始化阶段是执行类构造器()
方法的过程,那()
到底是什么呢?
java
在生成字节码时,如果类中有静态代码块或静态变量的赋值操作,会将类构造器()
方法和实例构造器 ()
方法添加到语法树中(可以理解为在编译阶段自动为类添加了两个隐藏的方法:类构造器——()
方法和实例构造器——()
方法,可以用javap
命令查看),()
主要用来构造类,比如初始化类变量(静态变量),执行静态代码块(statis{}
)等,该方法只执行一次;()
方法主要用来构造实例,在构造实例的过程中,会首先执行()
,这时对象中的所有成员变量都会被设置为默认值(每种数据类型的默认值和类加载准备阶段描述的一样),然后才会执行实例的构造函数(会先执行父类的构造方法,再执行非静态代码块,最后执行构造函数)。
(1)类加载器的作用
class
文件中获取,还可以从其他格式如zip
文件中读取、从网络或数据库中读取、运行时动态生成、由其他文件生成(比如jsp
生成class
类文件)等。从程序员的角度来看,类加载器动态加载class
文件到虚拟机中,并生成一个java.lang.Class
实例,每个实例都代表一个java
类,可以根据该实例得到该类的信息,还可以通过newInstance()
方法生成该类的一个对象。Java
虚拟机中的唯一性。也就是说,两个相同的类,只有是在同一个加载器加载的情况下才“相等”,这里的“相等”是指代表类的Class
对象的equals()
方法、isAssignableFrom()
方法、isInstance()
方法的返回结果,也包括instanceof
关键字对对象所属关系的判定结果。(2)类加载器的分类
以开发人员的角度来看,类加载器分为如下几种:启动类加载器(Bootstrap ClassLoader
)、扩展类加载器(Extension ClassLoader
)、应用程序类加载器(Application ClassLoader
)和自定义类加载器(User ClassLoader
),其中启动类加载器属于JVM
的一部分,其他类加载器都用java
实现,并且最终都继承自java.lang.ClassLoader
。
C/C++
编译而来的,看不到源码,所以在java.lang.ClassLoader
源码中看到的Bootstrap ClassLoader
的定义是native
的“private native Class findBootstrapClass(String name)
”。启动类加载器主要负责加载JAVA_HOME\lib
目录或者被-Xbootclasspath
参数指定目录中的部分类,具体加载哪些类可以通过“System.getProperty(“sun.boot.class.path”)
”来查看。JAVA_HOME\lib\ext
目录或者被java.ext.dirs
系统变量指定的路径中的所有类库,由sun.misc.Launcher.ExtClassLoader
实现,,可以用通过“System.getProperty(“java.ext.dirs”)
”来查看具体都加载哪些类。classpath
)上的类,由sun.misc.Launcher.AppClassLoader
实现,如果程序中没有自定义类加载器,应用程序类加载器就是程序默认的类加载器。JVM
提供的类加载器只能加载指定目录的类(jar
和class
),如果我们想从其他地方甚至网络上获取class
文件,就需要自定义类加载器来实现,自定义类加载器主要都是通过继承ClassLoader
或者它的子类来实现,但无论是通过继承ClassLoader
还是它的子类,最终自定义类加载器的父加载器都是应用程序类加载器,因为不管调用哪个父类加载器,创建的对象都必须最终调用java.lang.ClassLoader.getSystemClassLoader()
作为父加载器,getSystemClassLoader()
方法的返回值是sun.misc.Launcher.AppClassLoader
即应用程序类加载器。(3)ClassLoader与双亲委派模型
双亲委派模型的工作过程是,如果一个类加载器收到了类加载的请求,它首先不会加载类,而是把这个请求委派给它上一层的父加载器,每层都如此,所以最终请求会传到启动类加载器,然后从启动类加载器开始尝试加载类,如果加载不到(要加载的类不在当前类加载器的加载范围),就让它的子类尝试加载,每层都是如此。
那么双亲委派模型有什么好处呢?最大的好处就是它让Java中的类跟类加载器一样有了“优先级”。前面说到了对于每一个类,都需要由加载它的加载器和这个类本身共同确立这个类在Java虚拟机中的唯一性,比如java.lang.Object类(存放在JAVA_HOME\lib\rt.jar中),如果用户自己写了一个java.lang.Object类并且由自定义类加载器加载,那么在程序中是不是就是两个类?所以双亲委派模型对保证Java稳定运行至关重要。
每个对象都有一把锁。
线程的6种状态
线程的几个常见方法的比较
TIMED_WAITING
状态,但不释放对象锁,millis
后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。OS
再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()
达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()
不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。thread
的join
方法,当前线程进入WAITING/TIMED_WAITING
状态,当前线程不会释放已经持有的对象锁。线程thread
执行完毕或者millis
时间到,当前线程进入就绪状态。thread
的interrupt()
方法,中断指定的线程。wait()
方法组或者join
方法组在阻塞状态,那么指定线程会抛出InterruptedException
thread
的isInterrupted()
方法,返回指定线程是否被中断wait()
方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()
唤醒或者wait(long timeout) timeout
时间到自动唤醒。notifyAll()
唤醒在此对象监视器上等待的所有线程。volatile
当一个共享变量被volatile修饰之后, 其就具备了两个含义
volatile
关键字禁止指令重排序有两个含volatile
变量之前的语句不能在volatile
变量后面执行; 同样, 在volatile
变量之后的语句也不能在volatile变量前面执行。即该关键字保证了时序性。重入锁ReentrantLock
ReentrantLock reentrantLock = new ReentrantLock();
public void m() {
reentrantLock .lock();//1
reentrantLock .lock();//2
try {
// ... method body
} finally {
reentrantLock .unlock()
reentrantLock .unlock()
}
}
如果锁是不可以重入的,如上图,1获得了锁,2想要获得锁必须要1释放锁,但是1需要等到2执行完才释放锁,就陷入了锁死状态。重入锁概念如此,锁了多少次就要解锁所少次,不然这个锁不会释放。
重入锁synchronized
任意一个Java
对象,都拥有一组监视器方法(定义在java.lang.Object
上),主要包括wait()
、wait(long timeout)
、notify()
以及notifyAll()
方法,这些方法与synchronized
同步关键字配合,可以实现等待/通知模式。
synchronized(obj){
.... //1
obj.wait();//2
//obj.wait(long millis);//2
....//3
}
一个线程获得obj
的锁,做了一些时候事情之后,发现需要等待某些条件的发生,调用obj.wait()
,该线程会释放obj
的锁,并阻塞在上述的代码2
处。
synchronized(obj){
.... //1
obj.notify();//2
obj.notifyAll();//2
}
一个线程获得obj
的锁,做了一些时候事情之后,某些条件已经满足,调用obj.notify()
或者obj.notifyAll()
,该线程会释放obj
的锁,并叫醒在obj
上等待的线程。
Synchronized与Lock的区别
类别 | 存在层次 | 锁的释放 | 锁的获取 | 锁状态 | 锁类型 | 性能 |
---|---|---|---|---|---|---|
synchronized | Java的关键字,在jvm层面上 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 无法判断 | 可重入 不可中断 非公平 | 少量同步 |
Lock | 是一个接口 | 在finally中必须释放锁,不然容易造成线程死锁 | 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁) | 可以判断 | 可重入 可判断 可公平(两者皆可) | 大量同步。Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。 |
对象关系映射(Object Relational Mapping
,简称ORM
)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。本质上就是将数据从一种形式转换到另外一种形式。
JDBC
(Java Data Base Connectivity
,java
数据库连接)是一种用于执行SQL
语句的Java API
,可以为多种关系数据库提供统一访问,它由一组用Java
语言编写的类和接口组成。JDBC
提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。
https://developer.android.google.cn/jetpack
Lifecycle 是一个类,它持有关于组件(如 Activity 或 Fragment)生命周期状态的信息,并且允许其他对象观察此状态。释放onCreate() 和 onDestroy()等生命周期,简化View层的逻辑。
public interface IPresenter extends LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
void onCreate(@NotNull LifecycleOwner owner);
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
void onDestroy(@NotNull LifecycleOwner owner);
@OnLifecycleEvent(Lifecycle.Event.ON_ANY)
void onLifecycleChanged(@NotNull LifecycleOwner owner,@NotNull Lifecycle.Event event);
}
public class BasePresenter implements IPresenter {
@Override
public void onLifecycleChanged(@NotNull LifecycleOwner owner, @NotNull Lifecycle.Event event) {}
@Override
public void onCreate(@NotNull LifecycleOwner owner) {}
@Override
public void onDestroy(@NotNull LifecycleOwner owner) {}
}
这里我直接将我想要观察到Presenter
的生命周期事件都列了出来,然后封装到BasePresenter
中,这样每一个BasePresenter
的子类都能感知到Activity
容器对应的生命周期事件,并在子类重写的方法中,对应相应行为。
public class MainActivity extends AppCompatActivity {
private IPresenter mPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPresenter = new MainPresenter(this);
getLifecycle().addObserver(mPresenter);//添加LifecycleObserver
}
@Override
protected void onDestroy() {
super.onDestroy();
}
}
除onCreate
和onDestroy
事件之外,Lifecycle
一共提供了所有的生命周期事件,只要 通过注解进行声明,就能够使LifecycleObserver
观察到对应的生命周期事件。
反射Java层的异常抛出以及捕获系统信号量
除了下面提到的常规方案,还有字节码插桩的方案:在应用构建期间,通过修改字节码的方式来进行字节码插桩就是实现自动化的方案之一。我们想要对字节码进行修改,只需要在
javac
之后dex
之前遍历所有的字节码文件,并按照一定的规则过滤修改就好了,这里便是字节码插桩的入口。transform api
作为字节码插桩的入口。我们只需要实现一个自定义的Gradle Plugin
,然后在编译阶段去修改字节码文件。对于字节码的修改,比较常用的框架有Javassist
和ASM
。
一般从几下几个方面监控应用的性能:
Android
中可以获取CPU
总的使用时间,每个进程所用CPU
时间以及进程中每个线程的CPU
使用时间,它没有提供API
接口,可以直接在应用中读取/proc/stat
,/proc/pid/stat
,/proc/pid/task/tid/stat
这几个文件来获取,不需要root
权限。一些开源的性能统计工具也是通过这种方法来显示CPU
使用率的,比如网易的Emmagee
。
1、获取CPU总使用率
在proc/stat
中有详细的CPU
使用情况,详细格式如下:
CPU 16394633 4701297 9484146 36314562 70851 1797 202347 0 0 0
CPU后面的几位数字分别是
nice
值为负进程。nice
值为负的进程所占用的CPU
时间。在某个时间T1的CPU总时间为:
totalCpuTime1 = user1 + nice1 + system1 + idle1 + iowait1 + irq1 + softirq1
在时间T2的CPU总时间为(T2 > T1):
totalCpuTime2 = user2 + nice2 + system2 + idle2 + iowait2 + irq2 + softirq2
CPU总使用率的算法是:
100*((totalCpuTime2-totalCpuTime1) - (idel2-idel1)) / (totalCpuTime2-totalCpuTime1)
2、获取进程的CPU使用率
/proc/pid/stat
中则是该pid
进程的CPU
使用情况.详细格式如下:
2757 (zenmen.palmchat) S 549 549 0 0 -1 1077952832 1674191 173040 7885 4062265 19589 99 49412
其中粗体的四位数字分别是:
所以进程的CPU
使用时间processCpuTime
为这个四个属性的和.
//在T1时
processCpuTime1 = utime1 + stime1 + cutime1 + cstime1
//在T2时
processCpuTime2 = utime2 + stime2 + cutime2 + cstime2
所以进程所占CPU的使用率算法是:
100*(processCpuTime2-processCpuTime1) / (totalCpuTime2-totalCpuTime1)
3、获取进程中线程的CPU使用率
/proc/pid/task/tid/stat
中是该pid进程中tid
线程的CPU
使用情况,格式和进程CPU
使用的格式是一样的,
3810 (fileDecodingQue) S 549 549 0 0 -1 1077952576 269 173040 0 40 0 0 99 494 20 0
线程的CPU时间为:
threadCpuTime = utime + stime
线程的CPU使用率算法是:
100*(threadCpuTime2-threadCpuTime1) / (totalCpuTime2-totalCpuTime1)
Android
提供有API
可以获取系统总内存大小及当前可用内存大小,以及获取应用中内存的使用情况。
1、获取系统总内存大小及可用内存大小
ActivityManager.MemoryInfo
有以下几个Field
:
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo0 = new ActivityManager.MemoryInfo();
activityManager.getMemoryInfo(memoryInfo0);
Log.v("yhd-","availMem:"+memoryInfo0.availMem/(1024*1024));//MB
Log.v("yhd-","totalMem:"+memoryInfo0.totalMem/(1024*1024));//MB
2、获取应用使用的内存大小
Debug.MemoryInfo memoryInfo = new Debug.MemoryInfo();
Debug.getMemoryInfo(memoryInfo);
int totalPss = memoryInfo.getTotalPss();//返回的是KB
Android
系统从4.1(API 16)
开始加入Choreographer
这个类来控制同步处理输入(Input
)、动画(Animation
)、绘制(Draw
)三个操作。其实UI
显示的时候每一帧要完成的事情只有这三种。Choreographer
接收显示系统的时间脉冲(垂直同步信号-VSync
信号),在下一个帧渲染时控制执行这些操作。收到VSync
信号后,顺序执行3
个操作,然后等待下一个信号,再次顺序执行3
个操作。假设在第二个信号到来之前,所有的操作都执行完成了,即Draw
操作完成了,那么第二个信号来到时,此时界面将会更新为第一帧的内容,因为Draw
操作已经完成了。否则界面将不会更新,还是现实上一个帧的内容,表示丢帧了。丢帧是造成卡顿的原因。
通过 Choreographer
类设置它的 FrameCallback
,可以在每一帧被渲染的时候记录下它开始渲染的时间,每一次同步的周期为16ms
,代表一帧的刷新频率,一次界面渲染会回调 FrameCallback
的doFrame(longframeTimeNanos)
方法,如果两次 doFrame
之间的间隔大于16ms
说明丢帧了。用这种方法,可以实时监控应用的丢帧情况。
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
//Log.e("yhd-","frameTimeNanos:"+frameTimeNanos);
if (mLastFrameNanoTime != 0) {//mLastFrameNanoTime 上一次绘制的时间
long frameInterval = (frameTimeNanos - mLastFrameNanoTime)/1000000;//计算两帧的时间间隔ms
//如果时间间隔大于最小时间间隔,3*16.6ms,小于最大的时间间隔,60*16.6ms,就认为是掉帧,累加统计该时间。
//此处解释一下原因: 因为正常情况下,两帧的间隔都是在16.6ms以内 ,如果我们统计到的两帧间隔时间大于三倍的普通绘制时间,我们就认为是出现了卡顿。
//之所以设置最大时间间隔,是为了有时候页面不刷新绘制的时候,不做统计处理
if (frameInterval > (16.6*3) && frameInterval < 16.6*60) {
//做统计
Log.e("yhd-","frameInterval:"+frameInterval);
}
}
mLastFrameNanoTime = frameTimeNanos;
//注册下一帧回调
Choreographer.getInstance().postFrameCallback(this);
}
});
APP
进程CPU
时间,也可以针对一些场景手动获取。可以设定一个阈值,比如大于50%
的时候,将各个线程名及线程占用的CPU
时间一起保存到日志中。Activity start
时开始监控,当检测到丢帧时保存丢帆的个数,在Activity stop
时停止监控,取最大的丢帧个数,再将Activity
类名一起保存到日志中。下面以一个Socket
客服聊天系统协议实例说明
我们的客户端在读取服务器发来的数据时,可以通过以下方式阻塞:
InputStream in = null;
try {
//TCP套接字默认情况下是阻塞模式,也是最常用的,当然你也可以更改为非阻塞模式。
in = socket.getInputStream();
//in.read阻塞读,每个Socket被创建后,都会分配两个8192字节的8k缓冲区,输入缓冲区和输出缓冲区。
//write()/send()并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。
while (true) {
final int MLEN = 1024;
byte[] buff = new byte[MLEN];
//in.read()目前最大支持2048字节;这一方法的功能是从字节流中读取一个字节,若到了末尾则返回-1,否则返回读入的字节。
int size = in.read(buff);
//socket在发送方过快等情况下,会存在粘包情况,也就是一次读取中读出来了不止一个包。或者在发送包太大的情况下会存在分包的情况,需要做特殊处理。
//socket有个默认的超时断开时间(默认是60s),如果在设置的时间内一直没有数据传输,就会自动断开连接。所以要维持心跳,心跳的时间少于Socket内核超时断开连接时间。当然也可以把Socket可设为keepalive用不超时断开,但是官方不建议这么干。
}
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
我们知道,CPU
与内存发生的读写速度比硬件IO
快10
倍不止。为了提高速度,Socket
内核在内存中建立两个8k
输入输出缓冲区缓存区,write()/send()
并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP
协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP
协议负责的事情。当然你可以调用flush()
将数据立刻发送出去(InputStream
的flush
是空函数,子类才实现具体的方法)。TCP
协议独立于 write()/send()
函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。read()/recv()
函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。所以会存在粘包的现象,在发送数据过大的情况下也会存在分包的情况,都要处理好。我们用InputStream.read()
读包时,目前协议每次最大能读到2048
字节,读取成功返回读取到的字节数,socket
关闭返回-1
。
当发送的字节数据包比较大时,Socket
内部会将发送的字节数据进行分包处理,降低内存和性能的消耗。一个包没有固定长度,以太网限制在46-1500
字节,1500
就是以太网的MTU,超过这个量,TCP
会为IP
数据报设置偏移量进行分片传输。在分包产生时,要保留上一个包的部分内容,与下一个包的部分内容组合。
当发送的字节数据包比较小且频繁发送时,Socket
内部会将字节数据进行粘包处理,既将频繁发送的小字节数据打包成 一个整包进行发送,降低内存的消耗。在粘包产生时,要可以在同一个包内获取出多个包的内容。
Socket
有个默认的超时断开时间(默认是60s
),如果在设置的时间内一直没有数据传输,就会自动断开连接。现实场景我们并不想要这么快断开,所以要维持心跳(向Socket
发送任何数据维持),心跳的时间少于Socket
内核超时断开连接时间。当然也可以把Socket
可设为keepalive
永不超时断开,但是官方不建议这么干。
Java
中的字节流处理的最基本单位为单个字节,它通常用来处理二进制数据。Java
中最基本的两个字节流类是InputStream
和OutputStream
,它们分别代表了组基本的输入字节流和输出字节流。InputStream
类与OutputStream
类均为抽象类,我们在实际使用中通常使用Java
类库中提供的它们的一系列子类。InputStream.read()
是一个抽象方法,也就是说任何派生自InputStream
的输入字节流类都需要实现这一方法,这一方法的功能是从字节流中读取一个字节,若到了末尾则返回-1
,否则返回读入的字节。关于这个方法我们需要注意的是,它会一直阻塞直到返回一个读取到的字节或是-1
。另外,字节流在默认情况下是不支持缓存的,这意味着每调用一次read
方法都会请求操作系统来读取一个字节,这往往会伴随着一次磁盘IO,因此效率会比较低。要使用内存缓冲区以提高读取的效率,我们应该使用BufferedInputStream
。
BufferedInputStream
内部有一个缓冲区,默认大小为8M
,每次调用read
方法的时候,它首先尝试从缓冲区里读取数据,若读取失败(缓冲区无可读数据),则选择从物理数据源 (譬如文件)读取新数据(这里会尝试尽可能读取多的字节)放入到缓冲区中,最后再将缓冲区中的内容返回给用户.由于从缓冲区里读取数据远比直接从存储介质读取速度快,所以BufferedInputStream
的效率很高。
Java
中的字符流处理的最基本的单元是Unicode
码元(大小2
字节),它通常用来处理文本数据。所谓Unicode
码元,也就是一个Unicode
代码单元,范围是0x0000~0xFFFF
。在以上范围内的每个数字都与一个字符相对应,Java
中的String
类型默认就把字符以Unicode
规则编码而后存储在内存中。然而与存储在内存中不同,存储在磁盘上的数据通常有着各种各样的编码方式。使用不同的编码方式,相同的字符会有不同的二进制表示。实际上字符流是这样工作的:
Unicode
码元序列)转为指定编码方式下的字节序列,然后再写入到文件中;Unicode
码元序列从)从而可以存在内存中。由于字符流在输出前实际上是要完成Unicode
码元序列到相应编码方式的字节序列的转换,所以它会使用内存缓冲区来存放转换后得到的字节序列,等待都转换完毕再一同写入磁盘文件中。
为什么要使用字符流?
因为使用字节流操作汉字或特殊符号语言的时候容易乱码,因为汉字不止一个字节,为了解决这个问题,建议使用字符流。
什么情况下使用字符流?
一般可以用记事本打开的文件,我们可以看到内容不乱码的。就是文本文件,可以使用字符流。而操作二进制文件(比如图片、音频、视频)必须使用字节流。
主要使用的设计模式
处理流(包装流):并不直接连接数据源,是对一个已存在的流的连接和封装,是一种典型的装饰器设计模式,使用处理流主要是为了更方便的执行输入输出工作,如PrintStream
,输出功能很强大,又如BufferedReader
提供缓存机制,推荐输出时都使用处理流包装。
OTA
升级分为差分包升级和整包升级,不管是哪种都需要在源码中利用系统签名生成升级整包,差分包的话需要用命令生成带签名的差异部分。在用户升级判断中,需要先判断系统签名的正确性,然后写入临时标记升级包信息的文件,然后发送reboot(“recovery”)
命令(可选择是否擦除用户数据),重启进入recovery
模式,读取临时标记升级包信息的文件进行写入升级。
public static void installPackage(Context context, File packageFile)throws IOException {
//第一步验证签名
verifyPackage(packageFile,null,null);
String filePath = packageFile.getCanonicalPath();
String arg = "--update_package=" + filePath;
//第二部写入包信息临时标记文件
writeFlagCommand(filePath);
//第三部重启进入recovery升级
bootCommand(context, arg);
}
/**
* Verify the cryptographic signature of a system update package
* before installing it. Note that the package is also verified
* separately by the installer once the device is rebooted into
* the recovery system. This function will return only if the
* package was successfully verified; otherwise it will throw an
* exception.
*
* Verification of a package can take significant time, so this
* function should not be called from a UI thread. Interrupting
* the thread while this function is in progress will result in a
* SecurityException being thrown (and the thread's interrupt flag
* will be cleared).
*
* @param packageFile the package to be verified
* @param listener an object to receive periodic progress
* updates as verification proceeds. May be null.
* @param deviceCertsZipFile the zip file of certificates whose
* public keys we will accept. Verification succeeds if the
* package is signed by the private key corresponding to any
* public key in this file. May be null to use the system default
* file (currently "/system/etc/security/otacerts.zip").
*
* @throws IOException if there were any errors reading the
* package or certs files.
* @throws GeneralSecurityException if verification failed
*/
public static void verifyPackage(File packageFile,ProgressListener listener,File deviceCertsZipFile)
throws IOException, GeneralSecurityException {
//deviceCertsZipFile实际场景传null
}
/**
* Used to communicate with recovery. See bootable/recovery/recovery.c
*
* @param path the package path
*/
public static void writeFlagCommand(String path) throws IOException{
File UPDATE_FLAG_FILE = new File("/cache/recovery","last_flag");
FileWriter writer = new FileWriter(UPDATE_FLAG_FILE);
try {
writer.write("updating$path=" + path);
}finally {
writer.close();
}
}
/**
* Reboot into the recovery system with the supplied argument.
* @param arg to pass to the recovery utility.
* @throws IOException if something goes wrong.
*/
private static void bootCommand(Context context, String arg) throws IOException {
File COMMAND_FILE = new File("/cache/recovery", "command");
FileWriter command = new FileWriter(COMMAND_FILE);
try {
command.write(arg);
command.write("\n");
} finally {
command.close();
}
// Having written the command file, go ahead and reboot
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
pm.reboot("recovery");
throw new IOException("Reboot failed (no permissions?)");
}
/**
* Reboots the device and wipes the user data partition. This is
* sometimes called a "factory reset", which is something of a
* misnomer because the system partition is not restored to its
* factory state.
* Requires the {@link android.Manifest.permission#REBOOT} permission.
*
* @param context the Context to use
*
* @throws IOException if writing the recovery command file
* fails, or if the reboot itself fails.
*/
public static void rebootWipeUserData(Context context) throws IOException {
final ConditionVariable condition = new ConditionVariable();
Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
context.sendOrderedBroadcast(intent, android.Manifest.permission.MASTER_CLEAR,
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
condition.open();
}
}, null, 0, null, null);
// Block until the ordered broadcast has completed.
condition.block();
//重启进入recovery模式,下面讲
bootCommand(context, "--wipe_data");
}
开机logo
//1 、下载图片转换工具
sudo apt-get install pnmtoplainpm
//2、png转pnm
pngtopnm logo.png > logo_linux.pnm
//3、转化成224的pnm图片
pnmquant 224 logo_linux.pnm > logo_linux_clut224_formal.pnm
//4、转换成ppm格式
pnmtoplainpnm logo_linux_clut224_formal.pnm > logo_linux_clut224.ppm
//5、替换
/kernel/drivers/video/logo/logo_linux_clut224.ppm是默认的启动Logo图片,把自己的ogo_linux_clut224.ppm替换这个文件,同时删除logo_linux_clut224.c logo_linux_clut224.o文件(如果存在) 。
//6、编译kernel
make kernel.img
开机文字
system/core/init/init.c,如下代码片段,直接修改即可
if( load_565rle_image(INIT_IMAGE_FILE) ) {
fd = open("/dev/tty0", O_WRONLY);
if (fd >= 0) {
const char *msg;
msg = "\n"
"\n"
" A N D R O I D ";
write(fd, msg, strlen(msg));
close(fd);
}
} :
开机动画
动画文件(bootanimation.zip
)包含三个内容:两个目录part0
和part1
,一个文件desc.txt
。两个目录用来包含要显示的图片,分为第一阶段和第二阶段。剩下的文件就是设置关于如何显示的信息:
480 800 15
p 1 0 part0
p 0 0 part1
具体的含义如下:
480--图片的宽, 800--图片的高, 15--每一秒切换多少侦
p 1, 只循环一次
p 0, 无限循环直到系统开机
打包的方法:
zip -0 bootanimation.zip part0/*png part1/*png desc.txt
将bootanimation.zip
拷贝到自定义media
目录下,修改自己的makefile
文件,添加以下类似代码:
PRODUCT_COPY_FILES += \$(call find-copy-subdir-files,*,$(LOCAL_PATH)/media,system/media)
热修复技术可谓是百花齐放,阿里的Sophix
、微信的Tinker
、饿了么的Amigo
、美团的Robust
等等,下面以三个代表性作为对比。他们都支持兼容全部Android版本、加密传输及签名校验、支持服务端控制等。
方案对比 | Sophix | Tinker | Amigo |
---|---|---|---|
DEX修复 | 即时生效和冷启动修复 | 冷启动修复 | 冷启动修复 |
资源更新 | 差量包,不用合成 | 差量包,需要合成整包 | 全量包,不用合成 |
SO库更新 | 插桩实现,开发透明 | 替换so路径,开发不透明 | 插桩实现,开发透明 |
性能损耗 | 低,仅冷启动情况下有些损耗 | 高,有合成操作 | 低,全量替换 |
四大组件 | 不能新增 | 不能新增 | 能新增 |
生成补丁 | 直接选择已经编好的新旧包在本地生成 | 编译新包时设置基线包 | 上传完整新包到服务端 |
补丁大小 | 小 | 小 | 大 |
接入成本 | 傻瓜式接入 | 复杂 | 一般 |
是否收费 | 是 | 否 | 否 |
四大组件可以修改代码,但是无法做到新增。这是因为如果要新增四大组件,必须在
AndroidManifest
里面预先插入代理组件,并且尽可能声明所有权限,而这么做就会给原先的app
添加很多臃肿的代码,对app
运行流程的侵入性很强,所以,本着对开发者透明与代码极简的原则,没有做这种多余的处理。冷启动方式,指的是需要重启app
在下次启动时才能生效。
1、生成补丁流程
判断新旧文件的MD5
是否相等,不相等,说明有变化,自研DexDiff
差异算法,最后生成dex
文件的差异包,生成的文件以dex
结尾,但需要注意的是,它不是真正的dex
文件,只是个补丁包。其中新增的四大组件以及修改loader
相关的类是不允许的。
2、补丁包下发成功后合成全量Dex流程
当app
收到服务器下发的补丁后,首先检查补丁的合法性、签名、是否安装过补丁,检查通过后会针对old dex
和patch dex
进行合并,生成全量dex
文件,最终合成的文件会放到/data/data/${package_name}/tinker
目录下。
3、生成全量Dex后的加载流程
一个
ClassLoader
可以包含多个dex
文件,每个dex
文件是一个Element
,多个dex
文件排列成一个有序的数组dexElements
,当找类的时候,会按顺序遍历dex
文件,然后从当前遍历的dex
文件中找类,如果找类则返回,如果找不到从下一个dex
文件继续查找。
在TinkerApplication
中的onBaseContextAttached
中会通过反射调用TinkerLoader
的tryLoad
加载已经合成的dex
。先获取BaseDexClassLoader
的dexPathList
对象,然后通过dexPathList
的makeDexElements
函数将我们要安装的dex
转化成Element[]
对象,最后将其和dexPathList
的dexElements
对象进行合并,就是新的Element[]
对象,因为我们添加的dex都被放在dexElements
数组的最前面,所以当通过findClass
来查找这个类时,就是使用的我们最新的dex
里面的类。
Dalvik
下采用阿里自研的全量dex
方案:不是考虑把补丁包的dex
插到所有dex
前面(dex
插桩),而是想办法在原理的dex
中删除(只是删除了类的定义)补丁dex中存在的类,这样让系统查找类的时候在原来的dex中找不到,那么只有补丁中的dex
加载到系统中,系统自然就会从补丁包中找到对应的类。Art
下本质上虚拟机以及支持多dex
的加载,Sophix
的做法仅仅是把补丁dex
作为主dex(classes.dex)
而已,相当于重新组织了所有的dex
文件顺序:把补丁包的dex
改名为classes.dex
,以前apk
的所有dex
依次改为classes2.dex
、classes3.dex
… classesx.dex
。常用方案(Instant Run
技术):这种方案的兼容问题在于替换AssetManager
的地方。
1、资源补丁生成
ResDiffDecoder.patch(File oldFile, File newFile)
主要负责资源文件补丁的生成。如果是新增的资源,直接将资源文件拷贝到目标目录。如果是修改的资源文件则使用dealWithModeFile
函数处理。dealWithModeFile
会对文件大小进行判断,如果大于设定值(默认100Kb
),采用bsdiff
算法对新旧文件比较生成补丁包,从而降低补丁包的大小。如果小于设定值,则直接将该文件加入修改列表,并直接将该文件拷贝到目标目录,生成资源文件的补丁包。
2、补丁下发成功后资源补丁的合成
合成过程比较简单,没有使用bsdiff
生成的文件直接写入到resources.apk
文件;使用bsdiff
生成的文件则采用bspatch
算法合成资源文件,然后将合成文件写入resouces.apk
文件(是不是讲原始资源和补丁资源合成整个资源包还待验证)。最后,生成的resouces.apk
文件会存放到/data/data/${package_name}/tinker/res
对应的目录下。
3、资源补丁加载
资源补丁的加载的操作主要放在TinkerResourceLoader.loadTinkerResources
函数中,同dex
的加载时机一样,在app
启动时会被调用。我们获取资源的时候会调用getResource()
,Resources
对象的内部AssetManager
对象包含了framework
的资源还包含了应用程序本身的资源,因此这也就是为什么能使用getResources
函数获得的resources
对象来访问系统资源和本应用资源的原因。受此过程的提醒,我们是不是可以自己创建一个Resources
对象,让它的包含我们指定路径的资源,就可以实现访问其他的资源了呢?答案是肯定的,利用这个思想可以实现资源的动态加载,换肤、换主题等功能都可以利用这种方法实现。
主要思想就是创建一个AssetManager
对象,利用addAssetPath
函数添加指定的路径(addAssetPath
函数是hide
的,可以使用反射调用),用其创建一个Resources
对象,使用该Resources
对象获取该路径下的资源。
//path = getPackageManager().getApplicationInfo("xxx", 0).sourceDir;
public void loadRes(String path){
try {
assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, path);
} catch (Exception e) {
}
//这个resources就是我们最终用的资源文件了
resources = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
Sophix
方案没有直接使用Instant Run
的技术,而是另辟蹊径,构造了一个package id
为0x66
的资源包,这个包里只包含改变了的资源项,然后直接在原有AssetManager
中addAssetPath
这个包就可以了。由于补丁包的package id
为0x66
,不与目前已经加载的0x7f
冲突,因此直接加入到已有的AssetManager
中就可以直接使用了。补丁包里面的资源,只包含原有包里面没有而新的包里面有的新增资源,以及原有内容发生了改变的资源。并且,我们采用了更加优雅的替换方式,直接在原有的AssetManager
对象上进行析构和重构,这样所有原先对AssetManager
对象的引用是没有发生改变的,所以就不需要像Instant Run
那样进行繁琐的修改了。也不需要在运行时合成完整包。不占用运行时计算和内存资源。
补丁生成
生成补丁时比较新旧so
文件使用BSdiff
算法生成补丁包,
补丁合成
然后在下发补丁成功后根据BSpatch
算法将补丁包和旧的library
合成新的library
。并将更新后的Library库文件保存在tinker下面的目录下,这个目录就是/data/data/${package_name}/tinker/lib
。
补丁加载
首先讲下Android里面关于so的加载的三种方式:
System.loadLibrary
, 这种方式传入的是so
的名字,会直接从系统的目录去加载so文件,系统的路径包括/data/data/${package_name}/lib、/system/lib、/vender/lib
等这类路径。System.load
, 这种方式传入的是so
的绝对路径,直接从这个路径加载so
文件。so
库的路径插入到nativeLibraryDirectories
数组的最前面(nativeLibraryDirectories包括/data/data/${package_name}/lib
、/system/lib
、/vender/lib
的加载顺序),就能够达到加载so
库的时候是补丁so库,而不是原来so库的目录。Tinker
的so
文件热更新的原理就是通过方式二,直接加载新的so路径实现的。Tinker
中so
的热更新对用户并不是无感的,需要用户自发的去加载自己需要的库文件。为什么不第三种方式,官方给出解释:
但是
Tinker
并没有直接将补丁的lib路径添加到DexPathList
中,理论上这样可以做到程序完全没有感知的对Library
文件作补丁。这里主要是因为在多abi
的情况下,某些机器获取的并不准确。所以想要加载最新的库,需要自己使用TinkerInstaller.load*Library
去加载库文件,它会自动尝试先去Tinker
中的库文件加载,加载不成功会调用System.loadLibrary
调用系统的库文件。另外,对于第三方库文件的加载,Tinker
无法干预其加载时机,但是只要在我们的代码提前加载第三方的库文件即可。若想对第三方代码的库文件更新,可先使用TinkerInstaller.load*Library
对第三方库做提前的加载!当前使用方式似乎并不能做到开发者透明,这是因为Tinker
想尽量少的去hook
系统框架减少兼容性的问题。
//load lib/armeabi library
TinkerInstaller.loadArmLibrary(getApplicationContext(), "stlport_shared");
//load lib/armeabi-v7a library
TinkerInstaller.loadArmV7Library(getApplicationContext(), "stlport_shared");
Sophix
采用的是类似类修复反射注入方式。把补丁so
库的路径插入到nativeLibraryDirectories
数组的最前面,就能够达到加载so
库的时候是补丁so
库,而不是原来so
库的目录,从而达到修复的目的。
在
Java
运行时环境中,对于任意一个类,可以知道这个类有哪些属性和方法。对于任意一个对象,可以调用它的任意一个方法。这种动态获取类的信息以及动态调用对象的方法的功能来自于Java
语言的反射(Reflection)
机制。
package com.yhd.structure.utils;
public class Test {
public static boolean paramStatic = false;//静态成员变量
public boolean paramNormal = false;//成员变量
//这里要注意,如果带有static+final,变量类型是int、long、boolean以及String这些基本类型,
//JAVA在编译的时候为了优化效率就会把代码中对此常量中引用的地方替换成相应常量值,所以反射是无法修改的。
//如果是Integer、Long、Boolean这种包装类型,或者其他诸如Date、Object都是可以反射修改的。
private static final int IN_VALUE = 100;//static+final反射修改
private Test(String name){}
private static class InnerStaticClass {
private void print(String str) {}//静态内部类print带参方法
}
private class InnerNormalClass {
private void print(String str) {}//非静态内部类print带参方法
}
private Runnable runnable = () -> {};//匿名内部类run方法
}
反射的实例:
try {
//1、获取Class对象,每个Class在虚拟机中都是唯一的,可以去掉>
Class testClass = Class.forName("com.yhd.structure.utils.Test");//无法引用的时候只能用这种方式
//Class testClass = Test.class;//能够引用的时候可以用这种方式
//2、获取对象
Constructor constructor = testClass.getDeclaredConstructor(String.class);//读取构造函数
constructor.setAccessible(true);//忽略private等修饰符的限制
Object testObject = constructor.newInstance("testName");//通过构造函数获取类
//Object testObject = testClass.newInstance();//直接用默认构造函数,但是不能传递参数,不够灵活
//3、设置读取成员变量
Field normalField = testClass.getDeclaredField("paramNormal");//成员变量
normalField.setAccessible(true);//忽略private等修饰符的限制
normalField.set(testObject,true);//设置属性
boolean paramNormal = (boolean) normalField.get(testObject);//读取成员变量
//4、设置读取静态成员变量
Field staticField = testClass.getDeclaredField("paramStatic");//静态成员变量
staticField.setAccessible(true);//忽略private等修饰符的限制
staticField.set(null,true);//设置静态成员变量
boolean paramStatic = (boolean) staticField.get(null);//读取静态成员变量
//5、设置读取static+final常量
Field staticFinalField = testClass.getDeclaredField("IN_VALUE");//获取Test类的INT_VALUE字段
staticFinalField.setAccessible(true);//忽略private等修饰符的限制
//Field modifiersField = Field.class.getDeclaredField("modifiers");//modifiers属性已经找不到
//modifiersField.setAccessible(true);//忽略private等修饰符的限制
//modifiersField.setInt(staticFinalField, staticFinalField.getModifiers() & ~Modifier.FINAL);//去除final修饰符的影响,将字段设为可修改的
staticFinalField.set(null, 200);//把字段值设为200
//6、读取匿名内部类
Field field = testClass.getDeclaredField("runnable");//内部匿名类
field.setAccessible(true);//忽略private等修饰符的限制
Runnable runnable = (Runnable) field.get(testObject);//执行:
runnable.run();
//7、读取静态内部类
Class innerStaticClass = Class.forName("com.yhd.structure.utils.Test$InnerStaticClass");//直接反射静态内部类
Constructor innerStaticConstructor = innerStaticClass.getDeclaredConstructor();//获取构造函数必须传外部类的Class
innerStaticConstructor.setAccessible(true);
Object innerStaticObject = innerStaticConstructor.newInstance();//获取实例对象必须传外部类的实例
Method innerStaticMethod= innerStaticClass.getDeclaredMethod("print", String.class);
innerStaticMethod.setAccessible(true);//忽略private等修饰符的限制
innerStaticMethod.invoke(innerStaticObject, "I am InnerStaticClass");//直接调用
//8、读取内部类
Class innerNormalClass = Class.forName("com.yhd.structure.utils.Test$InnerNormalClass");//直接反射静态内部类
Constructor innerNormalConstructor = innerNormalClass.getDeclaredConstructor(testClass);//获取构造函数必须传外部类的Class,编译阶段决定了
innerNormalConstructor.setAccessible(true);
Object innerNormalObject = innerNormalConstructor.newInstance(testObject);//获取实例对象必须传外部类的实例
Method innerNormalMethod = innerNormalClass.getDeclaredMethod("print", String.class);//获取内部方法
innerNormalMethod.setAccessible(true);//忽略private等修饰符的限制
innerNormalMethod.invoke(innerNormalObject, "I am NormalClass");//执行函数
} catch (Exception e) {
e.printStackTrace();
}
反射不仅可以让我们获得隐藏的方法和属性,还可以让对象的实例化从编译时转化为运行时,因为我们可以通过Class.forName("xxx").newInstance()
的方法来生成新的实例,我们就可以很大程度上对程序应用中的功能模块进行解耦合。
JVM
无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。Applet
,那么这就是个问题了。getMethods()
后再遍历筛选,而直接用getMethod(methodName)
来根据方法名获取方法。joor
,或者apache
的commons
相关工具类。JDK
也很重要,反射性能一直在提高。使用https
是否就万事大吉?对于攻击者来说,你使用http
和https
是没有区别的。
使用抓包工具时,给目标设备安装并信任装包工具的自签名证书,这时候就可以分析https请求了。https
并不能阻挡攻击者分析请求接口并发起攻击,为了增加攻击者分析请求的难度,通常可以采用两种方式:
使用签名
。即给你的请求参数添加一个签名,后台服务接收到请求时,先验证签名,签名不正确的话,则不予处理。签名规则五花八门,大致策略就是根据请求参数做一些运算最后生成一个唯一的字符串当做sign,微信支付的签名大家可以做一个参考:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3数据加密
。post到服务器和从服务器返回的数据都做加密,这样的话即使攻击者拿到你的数据,他不知道你的加密算法就无能为力了。下面举例。DES
,3DES
,AES
;MD5
、SHA1
、CRC-32
。四种加密方法各有优缺点,在时实际应用中,数据从发送方到达接收方,通常是这样应用的:
S1
;S1
和数据进行对称加密生成S2
;S2
和对称加密的密码使用接收方的公钥进行加密。这样一来数据在传输过程中的完整性、保密性以及对发送方身份的验证都能得到保障。当数据到达接收方时,接收方先用自己的私钥对接收到的数据进行解密,文库得到密码和加密的数据;使用密码对加密数据解密,得到加密的特征码和数据;用发送方的公钥解密特征码,如果能解密,则说明该数据是由发送方所发;反之则不是,这便实现了身份验证;最后计算数据的特征码和解密出来的特征码做对比,如果一样,则该数据没有被修改;反之则数据被修改过了。
点续传也就是从下载断开的哪里,重新接着下载,直到下载完整/可用。从 HTTP1.1
协议开始就已经支出获取文件的部分内容,断点续传技术就是利用 HTTP1.1
协议的这个特点在 Header
里添加两个参数来实现的。这两个参数分别是客户端请求时发送的 Range
和服务器返回信息时返回的 Content-Range - Range
。Range
用于指定第一个字节和最后一个字节的位置,格式如下:
Range:(unit=first byte pos)-[last byte pos]
Range:bytes=0-1024
:表示传输的是从开头到第1024字节的内容;Range:bytes=1025-2048
:表示传输的是从第1025到2048字节范围的内容;Range:bytes=-2000
:表示传输的是最后2000字节的内容;Range:bytes=1024-
:表示传输的是从第1024字节开始到文件结束部分的内容;Range:bytes=0-0,-1
:表示传输的是第一个和最后一个字节 ;Range:bytes=1024-2048,2049-3096,3097-4096
:表示传输的是多个字节范围。Content-Range
用于响应带有 Range
的请求。服务器会将 Content-Range
添加在响应的头部,格式如下:
Content-Range:bytes(unit first byte pos)-[last byte pos]/[entity length]
比如:Content-Range:bytes 2048-4096/10240
,这里边 2048-4096
表示当前发送的数据范围, 10240
表示文件总大小。
返回的HTTP状态码意义:
200
:是OK
(一切正常),206
:是Partial Content
(服务器已经成功处理了部分内容)。416
:Requested Range Not Satisfiable
(对方(客户端)发来的Range
请求头不合理)。一般处理单线程处理:
客户端发来请求 ——-> 服务端返回200 ——> 客户端开始接受数据 ——> 用户手多多把下载停止了 ——> 客户端突然停止接受数据 ——> 然后客户端都没说再见就与服务端断开了 ——> 用户手的痒了又按回开始键 ——> 客户端再次与服务端连接上,并发送Range
请求头给服务端 ——> 这时服务端返回是206
(Range不合理返回416
) ——> 服务端从断开的数据那继续发送,并且会发送响应头:Content-Range
给客户端 ——>客户端接收数据 ——>直到完成。
这里的校验主要针对断点续传下载来说的。当服务器端的文件发生改变时,客户端再次向服务端发送断点续传请求时,数据肯定就会发生错误。这时我们可以利用 Last-Modified
来标识最后的修改时间,这样就可以判断服务器上的文件是否发生改变。和 Last-Modified
具有同样功能的还有 if-Modified-Since
,它俩的不同点是 Last-Modified
由服务器发送给客户端,而 if-Modified-Since
是由客户端发出, if-Modified-Since
将先前服务器发送给客户端的 Last-Modified
发送给服务器,服务器进行最后修改时间验证后,来告知客户端是否需要重新从服务器端获取新内容。客户端判断是否需要更新,只需要判断服务器返回的状态码即可,206
代表不需要重新获取接着下载就行,200
代表需要重新获取。
Bitmap
的创建非常消耗时间和内存,可能导致频繁GC
,使用缓存策略能够高效加载Bitmap
,减少卡顿,从而减少读取耗时和电量消耗。
Android原生为我们提供了一个LruCache
,其中维护着一个LinkedHashMap
,用url
(可能有特殊字符影响使用)的MD5
作为key。LruCache
可以用来存储各种类型的数据,但最常见的是存储图片(Bitmap
)。LruCache
创建时,我们需要设置它的大小,一般是系统最大存储空间的八分之一。LruCache
的机制是存储最近、最后使用的图片,如果LruCache
中的图片大小超过了其默认大小,则会将最老、最远使用的图片移除出去。
当图片被LruCache
移除的时候,我们需要手动将这张图片添加到软引用(SoftReference
)中。我们需要在项目中维护一个由SoftReference
组成的集合,其中存储被LruCache
移除出来的图片。软引用的一个好处是当系统空间紧张的时候,软引用可以随时销毁,因此软引用是不会影响系统运行的,换句话说,如果系统因为某个原因OOM
了,那么这个原因肯定不是软引用引起的。
当我们的APP
中想要加载某张图片时,先去LruCache
中寻找图片,如果LruCache
中有,则直接取出来使用,如果LruCache
中没有,则去SoftReference
中寻找,如果SoftReference
中有,则从SoftReference
中取出图片使用,同时将图片重新放回到LruCache
中,如果SoftReference
中也没有图片,则去文件系统中寻找,如果有则取出来使用,同时将图片添加到LruCache
中,如果没有,则连接网络从网上下载图片。图片下载完成后,将图片保存到文件系统中,然后放到LruCache
中。
React Native
继承了 React
在 JavaScript
的扩展语法 JSX
中直接以声明式的方式来描述 UI
结构的机制,并实现了一个 CSS
的子集,这把「DOM Representation」
的概念外扩成了「UI Representation」
,由于不是操作真实 UI
,就可以放到非 UI
线程来进行 render
——所有做客户端 UI
开发的都应该知道 UI
线程是永远的痛,无论你怎么 render
,render
的多低效,这些 render
都不会直接影响 UI
,而要借由 React Native
来将改变更新回 UI
线程。由于目前没有任何示例代码,也看不到更细节的实现机制介绍,所以以下部分为猜测。如果 React Native
沿袭 React
的机制,就会同样是把两次 render
的 diff
结果算出来,然后把 diff
结果传递回主线程,在最小范围内更新 UI
。
所以,核心是以下三点:
UI
线程渲染 UI Representation
diff
算法保证 UI update
的高效React
很有可能成为一个跨平台的统一 UI
解决方案,可以理解为 UI
开发的虚拟机。声明式 UI
开发,简单快捷,必然大有作为。精细控制无力,复杂应用场景无法很好满足,必然受限。
热加载的基础是模块热替换(HMR
,Hot Module Replacement
),HMR
最开始是由 Webpack
引入的,我们在 React Native Packager
中也实现了这个功能。HMR
使得 Packager
可以监控文件的改动并发送 HMR
更新消息(HMR update
)给包含在 APP
中的一层很薄的 HMR
运行时(HMR runtime
)。
简而言之,HMR
更新消息包含 JS
模块中发生变化的代码,当 HMR
运行时接收到这个消息,就使用新的代码替换旧的代码,流程如下图所示:
市面上也有比较成熟的热更新服务MicroSoft CodePush
和React Native中文网 pushy
,不过很多时候公司可能要搭建自己的热更新服务器,包括全量热更新和增量热更新。
bsdiff
算法将老RN
包和新RN
包生成一个补丁patch
文件,供客户端下载。客户端下载patch
文件,使用将补丁patch
文件和老RN
包生成一个新RN
包。微信小程序采用JavaScript
、WXML
、WXSS
三种技术进行开发,从技术讲和现有的前端开发差不多,但深入挖掘的话却又有所不同。
JavaScript
的代码是运行在微信App
中的,并不是运行在浏览器中,因此一些H5
技术的应用,需要微信App
提供对应的API
支持,而这限制住了H5
技术的应用,且其不能称为严格的H5
,可以称其为伪H5
,同理,微信提供的独有的某些API
,H5
也不支持或支持的不是特别好。WXML
微信自己基于XML
语法开发的,因此开发时,只能使用微信提供的现有标签,HTML
的标签是无法使用的。WXSS
具有CSS
的大部分特性,但并不是所有的都支持,而且支持哪些,不支持哪些并没有详细的文档。单位rpx
是响应式像素,可以根据屏幕宽度进行自适应。规定屏幕宽为 750rpx
。微信的架构,是数据驱动的架构模式,它的UI
和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现。
小程序分为两个部分webview
和appService
。其中webview
主要用来展现UI
,appService
有来处理业务逻辑、数据及接口调用。它们在两个进程中运行,通过系统层JSBridge
实现通信,实现UI
的渲染、事件的处理。
优势
App
低缺点
App
要深很多2M
。不能打开超过10
个层级的页面Flutter
的目标是使同一套代码同时运行在Android
和iOS
系统上,并且拥有媲美原生应用的性能,Flutter
甚至提供了两套控件来适配Android
和iOS
(Material
& Cupertino
),为了让App在细节处看起来更像原生应用。
在Flutter
诞生之前,已经有许多跨平台UI
框架的方案,比如基于WebView
的Cordova
、AppCan
等,还有使用HTML
+JavaScript
渲染成原生控件的React Native
、Weex
等。
基于WebView
的框架优点很明显,它们几乎可以完全继承现代Web开发的所有成果(丰富得多的控件库、满足各种需求的页面框架、完全的动态化、自动化测试工具等等),当然也包括Web
开发人员,不需要太多的学习和迁移成本就可以开发一个App。同时WebView
框架也有一个致命(在对体验&性能有较高要求的情况下)的缺点,那就是WebView
的渲染效率和JavaScript
执行性能太差。再加上Android
各个系统版本和设备厂商的定制,很难保证所在所有设备上都能提供一致的体验。
为了解决WebView
性能差的问题,以React Native
为代表的一类框架将最终渲染工作交还给了系统,虽然同样使用类HTML
+JS
的UI
构建逻辑,但是最终会生成对应的自定义原生控件,以充分利用原生控件相对于WebView
的较高的绘制效率。与此同时这种策略也将框架本身和App
开发者绑在了系统的控件系统上,不仅框架本身需要处理大量平台相关的逻辑,随着系统版本变化和API的变化,开发者可能也需要处理不同平台的差异,甚至有些特性只能在部分平台上实现,这样框架的跨平台特性就会大打折扣。
Flutter
则开辟了一种全新的思路,从头到尾重写一套跨平台的UI
框架,包括UI
控件、渲染逻辑甚至开发语言。渲染引擎依靠跨平台的Skia
图形库来实现,依赖系统的只有图形绘制相关的接口,可以在最大程度上保证不同平台、不同设备的体验一致性,逻辑处理使用支持AOT
的Dart
语言,执行效率也比JavaScript
高得多。
在Flutter
中,所有功能都可以通过组合多个Widget
来实现,包括对齐方式、按行排列、按列排列、网格排列甚至事件处理等等。Flutter控件主要分为两大类,StatelessWidget
和StatefulWidget
,StatelessWidget
用来展示静态的文本或者图片,如果控件需要根据外部数据或者用户操作来改变的话,就需要使用StatefulWidget
。
Flutter
的热重载是基于Debug
模式下的 JIT
编译模式的代码增量同步(而 Release
模式下采用的是 AOT
静态编译)。由于 JIT
属于动态编译,能够将 Dart
代码编译成生成中间代码,让 Dart VM
在运行时解释执行,因此可以通过动态更新中间代码实现增量同步。
热重载的流程可以分为 5
步,包括:扫描工程改动、增量编译、推送更新、代码合并、Widget
重建。Flutter
在接收到代码变更后,并不会让 App
重新启动执行,而只会触发 Widget
树的重新绘制,因此可以保持改动前的状态,大大缩短了从代码修改到看到修改产生的变化之间所需要的时间。
另一方面,由于涉及到状态的保存与恢复,涉及状态兼容与状态初始化的场景,热重载是无法支持的,如改动前后 Widget
状态无法兼容、全局变量与静态属性的更改、main
方法里的更改、initState
方法里的更改、枚举和泛型的更改等。
如果你在写业务逻辑的时候,不小心碰到了热重载无法支持的场景,也不需要进行漫长的重新编译加载等待,只要点击位于工程面板左下角的热重启(Hot Restart
)按钮,就可以以秒级的速度进行代码重新编译以及程序重启了,Hot Restart
可以完全重启您的应用程序,但却不用结束调试会话。
数据库的数据安全是无法做到绝对安全的,只能将密码等敏感数据放在服务器,客户端做数据库加密尽量加大被破解难度。连微信的数据库都被破解。
Android
平台自带的SQLite
有一个致命的缺陷:不支持加密。这就导致存储在SQLite
中的数据能够被不论什么人用不论什么文本编辑器查看到。假设是普通的数据还好,可是当涉及到一些账号password
,或者聊天内容的时候,我们的应用就会面临严重的安全漏洞隐患。
对数据库加密的思路有两种:
GeenDao
不支持整个数据库加密(难度较大),只能用这种方式。只是这样的方式并非彻底的加密。由于数据库的表结构等信息还是能被查看到。另外写入数据库的内容加密后,搜索也是个问题,解密过程性能也不好,只能敏感数据加密。SQLite
加密基本都是通过这样的方式实现的。微信也是采用SQLCipher
,他实质上是使用AES
加密的,但是也是可以破解的,只要逆向分析出秘钥,即使是so
文件也是可以逆向的,我们能做的就是加大逆向力度。微信的秘钥生成规则:
1、首先得到设备的IMEI和微信用户的uin
2、合并IMEI和uin,将合并后的字符串转为byte数组
3、然后对该byte数组计算MD5值,取计算出的MD5值的前7位作为数据库的密码
//获得当前CPU的核心数
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//设置线程池的核心线程数2-4之间,但是取决于CPU核数
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
//设置线程池的最大线程数为 CPU核数*2+1
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
//设置线程池空闲线程存活时间30s
private static final int KEEP_ALIVE_SECONDS = 30;
实例
/**
* 步骤1:创建AsyncTask子类
* 注:
* a. 继承AsyncTask类
* b. 为3个泛型参数指定类型;若不使用,可用java.lang.Void类型代替
* c. 根据需求,在AsyncTask子类内实现核心方法
*/
private class MyTask extends AsyncTask<String, Integer, String> {
// 作用:执行 线程任务前的操作
// 注:根据需求复写
@Override
protected void onPreExecute() {}
// 作用:接收输入参数、执行任务中的耗时操作、返回 线程任务执行的结果
// 注:必须复写,从而自定义线程任务
@Override
protected String doInBackground(String... params) {
// 自定义的线程任务
// 可调用publishProgress()显示进度, 之后将执行onProgressUpdate()
publishProgress(50);
return "result";
}
// 作用:在主线程 显示线程任务执行的进度
// 注:根据需求复写
@Override
protected void onProgressUpdate(Integer... progresses) {}
// 作用:在主线程,接收线程任务执行结果、将执行结果显示到UI组件
// 注:必须复写,从而自定义UI操作
@Override
protected void onPostExecute(String result) {}
// 作用:将异步任务设置为:取消状态
@Override
protected void onCancelled() {}
}
/**
* 步骤2:创建AsyncTask子类的实例对象(即 任务实例)
* 注:AsyncTask子类的实例必须在UI线程中创建
*/
MyTask mTask = new MyTask();
/**
* 步骤3:手动调用execute(Params... params) 从而执行异步线程任务
* 注:
* a. 必须在UI线程中调用
* b. 同一个AsyncTask实例对象只能执行1次,若执行第2次将会抛出异常
* c. 执行任务中,系统会自动调用AsyncTask的一系列方法:onPreExecute() 、doInBackground()、onProgressUpdate() 、onPostExecute()
* d. 不能手动调用上述方法
*/
mTask.execute("param");
public class HandlerThread extends Thread {
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
public Looper getLooper() {
if (!isAlive()) {
return null;
}
// If the thread has been started, wait until the looper has been created.
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}
}
两个方法中的synchronized
同步块,wait
和notifyAll
协作使用,保证了mLooper
创建的同步。
//创建一个线程,线程名字:handler-thread
myHandlerThread = new HandlerThread( "handler-thread") ;
//开启一个线程,必须执行start才能初始化looper
myHandlerThread.start();
//在这个线程中创建一个handler对象
handler = new Handler( myHandlerThread.getLooper() ){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//这个方法是运行在 handler-thread 线程中的 ,可以执行耗时操作
Log.d( "handler " , "消息: " + msg.what + " 线程: " + Thread.currentThread().getName() ) ;
}
};
//在主线程给handler发送消息
handler.sendEmptyMessage( 1 ) ;
//在子线程给handler发送消息
new Thread(-> handler.sendEmptyMessage(2)).start() ;
//释放资源
myHandlerThread.quit();
HandlerThread的特点:
HandlerThread
将loop
转到子线程中处理,说白了就是将分担MainLooper
的工作量,降低了主线程的压力,使主界面更流畅。HandlerThread
本质是一个线程,在线程内部,代码是串行处理的。HandlerThread
拥有自己的消息队列,它不会干扰或阻塞UI
线程。IO
操作,HandlerThread
并不适合,因为它只有一个线程,还得排队一个一个等着。IntentService
继承自service
,但是优先级高于Service
,比较适合执行一些高优先级的异步任务。内部是封装HandlerThread
和Handler
的。在IntentService
内有一个工作线程来处理耗时操作,启动IntentService
的方式和启动传统的Service
一样,并且,当任务执行完后,IntentService
会自动停止,而不需要我们手动去控制或者stopSelf
。另外,可以启动IntentService
多次,而每一个耗时操作会以工作队列的方式在IntentService
的onHandlerIntent
回调方法中执行(onHandlerIntent
是执行IntentService
耗时操作的一个方法),并且每次只会执行一个工作线程,执行完第一个再执行第二个,因为他是串行的。
public class MyIntentService extends IntentService {
public MyIntentService(String name) {
super(name);
}
public MyIntentService() {}
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void onHandleIntent(Intent intent) {
//内部采用HandlerThread机制,串行执行,在子线程中执行,执行完自动关闭销毁
handleUpload(path);
}
/**
* 处理上传操作
* @param path
*/
private void handleUpload(String path) {}
@Override
public void onDestroy() {
super.onDestroy();
}
}
与Broadcast
的Bindler
机制不同,LocalBroadcast
其实是handler
机制,这也就解释了为什么其仅能在进程内使用,也就是说,LocalBroadcast
虽然命名像是Broadcast
,但其并不是Broadcast
,也不属于四大组件,同样采用观察者模式以及单例模式。
IntentFilter filter = new IntentFilter();
filter.addAction("com.xxx.ACTION");
LocalBroadcastManager.getInstance(context).registerReceiver(receiver,filter);
每个Android
的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB
的虚拟地址空间,其中3GB
是用户空间,1GB
是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client
进程向Server
进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client
端与Server
端进程往往采用ioctl
等方法跟内核空间的驱动进行交互。
Android
系统是基于 Linux
内核的,Linux
已经提供了管道、消息队列、共享内存和 Socket
等 IPC
机制。那为什么 Android
还要提供 Binder
来实现 IPC
呢?主要是基于性能、稳定性和安全性几方面的原因。
1、性能
首先说说性能上的优势。Socket
作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。Binder
只需要一次数据拷贝,性能上仅次于共享内存。
2、稳定性
再说说稳定性,Binder
基于 C/S
架构,客户端(Client
)有什么需求就丢给服务端(Server
)去完成,架构清晰、职责明确又相互独立,自然稳定性更好。共享内存虽然无需拷贝,但是控制负责,难以使用。从稳定性的角度讲,Binder
机制是优于内存共享的。
3、安全性
传统的进程通信方式对于通信双方的身份并没有做出严格的验证,比如Socket
通信ip
地址是客户端手动填入,很容易进行伪造,而Binder
机制从协议本身就支持对通信双方做身份校检,因而大大提升了安全性。
进程之间通信的通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copy_from_user
() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy_to_user
() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信
传统的 IPC
机制如管道、Socket
都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder
并不是 Linux
系统内核的一部分,那怎么办呢?这就得益于 Linux
的动态内核可加载模块(Loadable Kernel Module,LKM
)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android
系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。
在
Android
系统中,这个运行在内核空间,负责各个用户进程通过Binder
实现通信的内核模块就叫Binder
驱动(Binder Dirver
)。
Binder IPC
机制中涉及到的内存映射通过 mmap()
来实现,mmap()
是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。
Binder IPC
正是基于内存映射(mmap
)来实现的,一次完整的 Binder IPC
通信过程通常是这样:
Binder
驱动在内核空间创建一个数据接收缓存区;copy_from_user()
将数据 copy
到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。前面我们介绍过,Binder
是基于 C/S
架构的。由一系列的组件组成,包括 Client
、Server
、ServiceManager
、Binder
驱动。其中 Client
、Server
、Service
Manager
运行在用户空间,Binder
驱动运行在内核空间。其中 Service Manager
和 Binder
驱动由系统提供,而 Client
、Server
由应用程序来实现。Client
、Server
和 ServiceManager
均是通过系统调用 open
、mmap
和 ioctl
来访问设备文件 /dev/binder
,从而实现与 Binder
驱动的交互来间接的实现跨进程通信。
至此,我们大致能总结出 Binder
通信过程:
BINDER_SET_CONTEXT_MGR
命令通过 Binder
驱动将自己注册成为 ServiceManager
;Server
通过驱动向 ServiceManager
中注册 Binder
(Server
中的 Binder
实体),表明可以对外提供服务。驱动为这个 Binder
创建位于内核中的实体节点以及 ServiceManager
对实体的引用,将名字以及新建的引用打包传给 ServiceManager
,ServiceManger
将其填入查找表。Client
通过名字,在 Binder
驱动的帮助下从 ServiceManager
中获取到对 Binder
实体的引用,通过这个引用就能实现和 Server
进程的通信。Binder
驱动的接入。下图能更加直观的展现整个通信过程(为了进一步抽象通信过程以及呈现上的方便,下图我们忽略了 Binder
实体及其引用的概念)。Android中关于锁有java.util.concurrent.locks
包下的锁以及synchronized
。
其中concurrent
下的锁有Lock
和ReadWriteLock
两个接口,他们的唯一实现类分别为ReentrantLock
可重入锁和ReentrantReadWriteLock
可重入读写锁。我们以ReentrantLock
为例做分析原理,ReentrantReadWriteLock
类似。
锁的存储结构就两个东西:“双向链表” + “int类型状态”,他们的变量都被transient
(去序列化)和volatile
(内存可见以及禁止指令重排)修饰。
Lock
的存储结构:一个int
类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)Lock
获取锁的过程:本质上是通过CAS
(乐观锁)来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。Lock
释放锁的过程:修改状态值,调整等待链表。Lock
大量使用CAS
+自旋。Lock和synchronized的选择
Lock
是一个接口,而synchronized
是Java
中的关键字,synchronized
是内置的语言实现;synchronized
在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock
在发生异常时,如果没有主动通过unLock()
去释放锁,则很可能造成死锁现象,因此使用Lock
时需要在finally
块中释放锁;Lock
可以让等待锁的线程响应中断,而synchronized
却不行,使用synchronized
时,等待的线程会一直等待下去,不能够响应中断;Lock
可以知道有没有成功获取锁,而synchronized
却无法办到。Lock
可以提高多个线程进行读操作的效率。synchronized
就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock
和ReentrantReadWriteLock
,它默认情况下是非公平锁,但是可以设置为公平锁。Lock
的性能要远远优于synchronized
。所以说,在具体使用时要根据适当情况选择。ReadWriteLock读写锁的使用
Java
并发库中ReetrantReadWriteLock
实现了ReadWriteLock
接口并添加了可重入的特性。ReetrantReadWriteLock
读写锁的效率明显高于synchronized
关键字ReetrantReadWriteLock
读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。ReetrantReadWriteLock
读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。至少四个,分别为主线程、与SyetemService交互线程、Window的页面绘制线程以及守护线程。
USER PID TID PPID VSZ RSS WCHAN ADDR S CMD
u0_a239 7532 7532 633 4468008 123960 0 0 S tudyproject.app(main)
u0_a239 7532 7547 633 4468008 123960 0 0 S Binder:7532_1 (ApplicatoinThread,与SystemService进行binder交互)
u0_a239 7532 7548 633 4468008 123960 0 0 S Binder:7532_2 (ViewRoot.W,负责页面绘制)
u0_a239 7532 7549 633 4468008 123960 0 0 S Binder:7532_3 (守护线程)
------------------below is the JVM(ART or Davlik) Thread -----------------------------
u0_a239 7532 7554 633 4468008 123960 0 0 S RenderThread (Surface thread)
u0_a239 7532 7538 633 4468008 123960 0 0 S Jit thread pool(JIT)
u0_a239 7532 7540 633 4468008 123960 0 0 S Signal Catcher(Linux Signal Recevier Thread)
u0_a239 7532 7541 633 4468008 123960 0 0 S ADB-JDWP Connected (DDMS 链接线程)
u0_a239 7532 7542 633 4468008 123960 0 0 S ReferenceQueueDaemon (引用队列Dameon 查看Daemons.java)
u0_a239 7532 7543 633 4468008 123960 0 0 S FinalizerDaemon (析构守护线程 调用对象finalizer())
u0_a239 7532 7544 633 4468008 123960 0 0 S FinalizerWatchdogDaemon (析构监控守护线程)
u0_a239 7532 7545 633 4468008 123960 0 0 S HeapTaskDaemon(堆剪裁守护线程)
u0_a239 7532 7550 633 4468008 123960 0 0 S Profile Saver (Profile Thread)
u0_a239 7532 7553 633 4468008 123960 0 0 S Binder:intercept (Dont know now)
u0_a239 7532 7656 633 4468008 123960 0 0 S queued-work-loop (Dont know now)
https://www.cnblogs.com/chenjunwu/p/10824688.html
https://www.cnblogs.com/nibolyoung/p/10954744.html
RecyclerView为什么强制我们实现ViewHolder模式
ListView
使用ViewHolder
的好处就在于可以避免每次getView
都进行findViewById()
操作,因为findViewById()
利用的是DFS
算法(深度优化搜索),是非常耗性能的。而对于RecyclerView
来说,强制实现ViewHolder
的其中一个原因就是避免多次进行findViewById
的处理。ItemView
和ViewHolder
的关系是一对一,也就是说一个ViewHolder
对应一个ItemView
。这个ViewHolder
当中持有对应的ItemView
的所有信息,比如说:position
、view
、width
等等,拿到了ViewHolder
基本就拿到了ItemView
的所有信息,而ViewHolder
使用起来相比itemView
更加方便。RecyclerView
缓存机制缓存的就是ViewHolder
(ListView
缓存的是ItemView
)Listview的缓存机制
ListView
的缓存有两级,在ListView
里面有一个内部类 RecycleBin
,RecycleBin
有两个对象Active View
和Scrap View
来管理缓存,Active View
是第一级,Scrap View
是第二级。
ItemView
,当列表数据发生变化时,屏幕内的数据可以直接拿来复用,无须进行数据绑定。ItemView
,这里所有的缓存的数据都是"脏的",也就是数据需要重新绑定,也就是说屏幕外的所有数据在进入屏幕的时候都要走一遍getView()
方法。RecyclerView
的缓存分为四级:ListView
的Active View
,就是屏幕内的缓存数据,就是相当于换了个名字,可以直接拿来复用。2
个,当其容量被充满同时又有新的数据添加的时候,会根据FIFO
原则,把优先进入的缓存数据移出并放到下一级缓存中,然后再把新的数据添加进来。Cache
里面的数据是干净的,也就是携带了原来的ViewHolder
的所有数据信息,数据可以直接来拿来复用。google
留给开发者自己来自定义缓存的,这个ViewCacheExtension
我个人建议还是要慎用。RecycledViewPool
:刚才说了Cache
默认的缓存数量是2
个,当Cache
缓存满了以后会根据FIFO
(先进先出)的规则把Cache
先缓存进去的ViewHolder
移出并缓存到RecycledViewPool
中,RecycledViewPool
默认的缓存数量是5
个。RecycledViewPool
与Cache
相比不同的是,从Cache
里面移出的ViewHolder
再存入RecycledViewPool
之前ViewHolder
的数据会被全部重置,相当于一个新的ViewHolder
,而且Cache
是根据position
来获取ViewHolder
,而RecycledViewPool
是根据itemType
获取的,如果没有重写getItemType
方法,itemType
就是默认的。因为RecycledViewPool
缓存的ViewHolder
是全新的,所以取出来的时候需要走onBindViewHolder
方法。Android
为移动操作系统特意编写了一些更加高效的容器SparseArray
和 ArrayMap
,在特定情况下用来替换HashMap
数据结构。合理运用这些数据结构将为我们节省内存空间但是牺牲效率,采用时间换空间的做法。在数据量1000
级以下,效率问题并不明显,但是超过1000
或者在倒序插入的情况下,SparseArray
效率降低比较明显。在正序插入的情况下,SparseArray
可以突破1000
,效率反而比HashMap
高。
创建一个 HashMap
时, 默认是一个容量为16
的数组,数组中的每一个元素却又是一个链表的头节点。或者说HashMap
内部存储结构 是使用哈希表的拉链结构(数组+链表,这种存储数据的方法 叫做拉链法。一旦数据量达到HashMap
限定容量的75%
,就将按两倍扩容,当我们有几十万、几百万数据时,HashMap
将造成内存空间的消耗和浪费。HashMap
获取数据是通过遍历Entry[]
数组来得到对应的元素,效率方面比较稳定。
默认容量10
,也可以指定。每次增加元素,size++
。因为它避免了对key
的自动装箱(int
转为Integer
类型),它内部则是通过两个数组来进行数据存储的,一个存储key
,另外一个存储value
,为了优化性能,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间。
SparseArray
在put
添加数据的时候,会使用二分查找法和之前的key
比较当前我们添加的元素的key
的大小,然后按照从小到大的顺序排列好,在正序插入的情况下效率高,但是反序下效率非常低。 而在获取数据的时候,也是使用二分查找法判断元素的位置,所以,在获取数据的时候非常快,比HashMap
快的多,因为HashMap
获取数据是通过遍历Entry[]
数组来得到对应的元素。
SparseArray应用场景:
虽说SparseArray比较省内存,但是由于其添加、查找、删除数据都需要先进行一次二分查找,所以在数据量大的情况下性能并不明显,将降低至少50%
。满足下面两个条件我们可以使用SparseArray
代替HashMap
:
key
必须为int
类型,这中情况下的HashMap
可以用SparseArray
代替。ArrayMap
是一个<key
,value
>映射的数据结构,key
可以为任意的类型,它设计上更多的是考虑内存的优化,内部是使用两个数组进行数据存储,一个数组记录key
的hash
值,另外一个数组记录Value
值,它和SparseArray
一样,也会对key
使用二分法进行从小到大排序,在添加、删除、查找数据的时候都是先使用二分查找法得到相应的index
,然后通过index
来进行添加、查找、删除等操作,所以,应用场景和SparseArray
的一样,如果在数据量比较大的情况下,那么它的性能将退化至少50%
。
ArrayMap应用场景:
在一个典型的显示系统中,一般包括
CPU
、GPU
、display
三个部分,CPU
负责计算数据,把计算好数据交给GPU
,GPU
会对图形数据进行渲染,渲染好后放到buffer
里存起来,然后display
(有的文章也叫屏幕或者显示器)负责把buffer
里的数据呈现到屏幕上。显示过程,简单的说就是CPU/GPU
准备好数据,存入buffer
,display
每隔一段时间去buffer
里取数据,然后显示出来。display
读取的频率是固定的,比如每个16.6ms
读一次,但是CPU/GPU
写数据是完全无规律的。
Android
每隔 16.6 ms
刷新一次屏幕其实是指底层会以这个固定频率来切换每一帧的画面。app
绘制视图树(View
树)计算而来的,这个工作是交由 CPU
处理,耗时的长短取决于我们写的代码:布局复不复杂,层次深不深,同一帧内刷新的 View
的数量多不多。CPU
绘制视图树来计算下一帧画面数据的工作是在屏幕刷新信号来的时候才开始工作的,而当这个工作处理完毕后,也就是下一帧的画面数据已经全部计算完毕,也不会马上显示到屏幕上,而是会等下一个屏幕刷新信号来的时候再交由底层将计算完毕的屏幕画面数据显示出来。app
界面不需要刷新时(用户无操作,界面无动画),app
就接收不到屏幕刷新信号所以也就不会让 CPU
再去绘制视图树计算画面数据工作,但是底层仍然会每隔 16.6 ms 切换下一帧的画面,只是这个下一帧画面一直是相同的内容。BitmapFactory.Options
有这么一个参数 ,可以重用一个bitmap
的内存去存放解析出另外一个新的bitmap
,但是有一定的要求:4.4
以上,只需要old bitmap
字节数比将要加载的bitmap
所需的字节数大,但是低于4.4
,要满足和待加载bitmap
长宽像素一致即可 (更加苛刻)。我们加载gif的png序列,每张图片都是一样大小,显然,符合这个所有特性(长宽一致)。bitmap
内存的重用,使得加载新bitmap
的时候不用重新分配内存,节省了一定的时间。
首先将Activity
用弱引用(WeakReference
)包装并绑定引用队列(ReferenceQueue
),onDestroy
以后,一旦主线程空闲下来,延时5
秒执行一个任务:先判断Activity
有没有被回收?如果已经回收了,说明没有内存泄漏,如果还没回收,我们进一步确认,手动触发一下gc
,然后再判断有没有回收,如果这次还没回收,说明Activity
确实泄漏了,接下来把泄漏的信息展示给开发者就好了。
它的基本工作原理如下:
RefWatcher.watch()
创建一个 KeyedWeakReference
到要被监控的对象。GC
。heap
内存 dump
到 APP
对应的文件系统中的一个 .hprof
文件中。HeapAnalyzerService
有一个 HeapAnalyzer
使用HAHA
解析这个文件。reference key
, HeapAnalyzer
找到 KeyedWeakReference
,定位内存泄漏。HeapAnalyzer
计算 到 GC roots
的最短强引用路径,并确定是否是泄漏。如果是的话,建立导致泄漏的引用链。APP
进程中的 DisplayLeakService
, 并以通知的形式展示出来。‘RecyclerView
布局等高,然后设置recyclerView.setHasFixedSize(true)
这样可以避免每次绘制Item
时,不再重新计算Item高度。Button
设置点击事件的时候都是直接创建一个匿名内部类的对象(new OnClickListener{}
),习惯了这种绑定事件的方式后我们可能在列表中也这么做。在列表滑动的时候会不停的重复创建新的OnClickListener
的操作,旧的OnClickListener
会被标记为需要垃圾回收,当需要回收的对象过多的时候会引起GC
,导致列表卡顿。可以创建一个通用的OnClickListener
,把数据放入Button
的Tag
中,根据id
来判断是哪个Button
执行了点击,来取出数据、执行不同的逻辑。ViewHolder
复用的出现是为了解决在绑定视图数据的时候使用findViewById
遍历视图树(以深度优先的方式)查找视图引起耗时操作的问题,将第一次查找到的视图放入静态的ViewHolder
中,以后绑定新数据时直接从ViewHolder
中拿到视图引用。notifyDataSetChanged()
之前在后台线程做好,在Adapter中只做数据的绑定。View
的显隐,当给一个View
设置setVisibility(View.GONE)
的时候,会触发布局的重新测量、布局、绘制等操作,若itemView
的布局比较复杂,重新测量绘制会很耗时间,引起列表卡顿。这个时候可以将数据和itemView
分解成不同的类型,根据类型来绑定对应的itemView
,减少布局的重绘操作。Glide
、Picasso
、Fresco
等图片异步加载框架。notifyDataSetChanged()
,而是使用notifyItemRangeInserted(rangeStart, rangeEnd)
。OOM for Perm (java.lang.OutOfMemoryError: PermGen space)
对应JVM
中的方法区的内存分配溢出。PermGen space
的全称是Permanent Generation space
,是指内存的永久保存区域。这块内存主要是被JVM
存放Class
和Meta
信息的,Class
在被Loader
时就会被放到PermGen space
中,它和存放类实例(Instance
)的Heap
区域不同,GC(Garbage Collection)
不会在主程序运行期对PermGen space
进行清理,所以如果你的应用中有很多CLASS
的话,就很可能出现PermGen space
错误。
OOM for Heap (java.lang.OutOfMemoryError: Java heap space)
对应JVM
中的堆区的内存分配溢出。Android
的虚拟机是基于寄存器的Dalvik
,它的最大堆大小一般是16M
,有的机器为24M
。我们平常看到的OutOfMemory
的错误,通常 是堆内存溢出。
OOM for StackOverflowError (Exception in thread “main” java.lang.StackOverflowError)
对应JVM
中的虚拟机栈的内存分配溢出。如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError
异常。如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
异常。一般在单线程程序情况下无法产生OutOfMemoryError
异常,使用多线程方式也会出现OutOfMemeoryError
,因为栈是线程私有的,线程多也会方法区溢出。
OOM for exhausted native memory (java.lang.OutOfMemoryErr java.io.FileInputStream.readBytes(Native Method))
对应JVM
中的本地方法区的内存分配溢出。导致OOM
的原因是由Native Method
的调用引起的,另外检查Java heap
,发现heap
的使用正常,因而需要考虑问题的发生是由于Native memory
被耗尽导致的。