Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社区中热度一直很高,所以决定花时间来了解和学习,为自己做技术储备。
首先声明,Spring Boot不是一门新技术,所以不用紧张。从本质上来说,Spring Boot就是Spring,它做了那些没有它你也会去做的Spring Bean配置。它使用“习惯优于配置”(项目中存在大量的配置,此外还内置了一个习惯性的配置,让你无需手动进行配置)的理念让你的项目快速运行起来。使用Spring Boot很容易创建一个独立运行(运行jar,内嵌Servlet容器)、准生产级别的基于Spring框架的项目,使用Spring Boot你可以不用或者只需要很少的Spring配置。
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>本文将讲解app的升级与更新。一般而言用户使用App的时候升级提醒有两种方式获得:
而更新操作一般是在用户点击了升级按钮之后开始执行的,这里的升级操作也分为两种形式:
app升级操作:
在App Store中升级需要为App Store上传新版App,我们在新版本完成之后都会上传到App Store中,在审核完成之后就相当于完成了这个应用市场的发布了,也就是发布上线了。这时候如果用户安装了这个应用市场,那么就能看到我们的App有新版本的升级提醒了。
除了可以在应用市场升级,我们还可以在应用内升级,在应用内升级主要是通过调用服务器端接口获取应用的升级信息,然后通过获取的服务器升级应用信息与本地的App版本比对,若服务器下发的最新的App版本高于本地的版本号,则说明有新版本发布,那么我们就可以执行更新操作了,否则忽略掉即可。
显然应用市场提醒的升级不是我们的重点,本篇主要是对于app升级的场景来进行不同角度的实现,便于以后开发过程中直接拿去用就ok了。
|
|
一般作为一个安卓程序员要测试还得写一个服务端(醉了),这里我就使用nodejs来搞一个本地的服务器来测试下app的版本更新检验。
|
|
然后我电脑上是装了webstrom的,没有装也没有关系但是必须有nodejs,现在都自带了express,表示并没有学过,所以简单的写个express_demo.js:
|
|
有webstrom的直接选中文件run就ok了,没有直接 node express_demo.js,可以直接浏览器打开:http://127.0.0.1:8081/update
上图为打开浏览器后的显示结果。
上图为webstrom的终端显示结果。
我们知道不同的需求有不同的操作方法和界面显示:
app内下载更新
这时我们必须等下载安装完全后才能进行操作,效果是这样的:
通知栏下载更新
这种情况是不在应用内更新,放在通知栏并不会影响当前app的使用,效果是这样的:
app更新分3种:强制更新,推荐更新,无需更新
强制更新
推荐更新
无需更新
具体思路:
- 实现bean用于对接后端接口实现app的更新(不写网络请求模拟本地数据也需要这个模型)
- 使用retrofit来请求版本更新接口
- 下载apk我们分别使用DownloadManager和普通的httpurlconnection
- 通过BroadcastReceiver来监听是否下载完成
首先我们要去解析服务端给的json,那么我们就要来创建一个bean类了,这里是严格根据json文件的格式来的:
|
|
这里使用retrofit和rxjava来练笔
先加入 依赖
|
|
接下来网络接口的定制:
|
|
通过工厂模式来创建ApiService :
|
|
版本检测接口的使用:
|
|
以上就是版本更新接口的调用,具体的rxjava+retrofit请自行学习你真的会用Retrofit2吗?Retrofit2完全教程
附上结果回调监听:
|
|
具体使用接口的处理:
|
|
实在不想写网络也好,直接使用假想数据做相关操作如下:
|
|
更新dialog的使用注意:
|
|
上面以强制更新举个例子,因为AlertDialog在不同的版本下面表现的美观度不一致,所以我们需要
12 > import android.support.v7.app.AlertDialog;>
然后显然是不能按返回键取消的,我们需要
12 > .setCancelable(false)>
Android自带的DownloadManager模块来下载,在api level 9之后,我们通过通知栏知道, 该模块属于系统自带, 它已经帮我们处理了下载失败、重新下载等功能。整个下载 过程全部交给系统负责,不需要我们过多的处理。
DownLoadManager.Query:主要用于查询下载信息。
DownLoadManager.Request:主要用于发起一个下载请求。
先看下简单的实现:
创建Request对象的代码如下:
|
|
取得系统服务后,调用downloadmanager对象的enqueue方法进行下载,此方法返回一个编号用于标示此下载任务:
|
|
这里我们可以看下request的一些属性:
|
|
具体实现思路:
我们通过downloaderManager来下载apk,并且本地保存downManager.enqueue(request)返回的id值,并且通过这个id获取apk的下载文件路径和下载的状态,并且通过状态来更新通知栏的显示。
第一次下载成功,弹出安装界面
如果用户没有点击安装,而是按了返回键,在某个时候,又再次使用了我们的APP
如果下载成功,则判断本地的apk的包名是否和当前程序是相同的,并且本地apk的版本号大于当前程序的版本,如果都满足则直接启动安装程序。
具体代码实现:
文件下载管理的实现,包括创建request和加入队列下载,通过返回的id来获取下载路径和下载状态。
|
|
app的检测安装的实现:
|
|
上面的代码可知:我们通过获取当前app的信息来比较是否需要下载和是否立即安装。第一次下载把downloadId保存到本地,用户下次进来的时候,取出保存的downloadId,然后通过downloadId来获取下载的状态信息。如果下载失败,则重新下载并且把downloadId存起来。如果下载成功,则判断本地的apk的包名是否和当前程序是相同的,并且本地apk的版本号大于当前程序的版本
,如果都满足则直接启动安装程序。
|
|
DownloadManager下载完成后会发出一个广播 android.intent.action.DOWNLOAD_COMPLETE
新建一个广播接收者即可:
清单配置:
先添加网络下载的权限:
|
|
再添加静态广播:
|
|
这种情况下载的话我们就不需要考虑id的问题,因为是直接在项目中下载,所以我们就是一个网络下载的过程,并且使用ProgressDialog显示下载信息及进度更新就ok了。
|
|
基本上具体的代码就写完了,但是说如果停止了下载管理程序
调用dm.enqueue(req);就会上面的错误,从而程序闪退.
所以在使用该组件的时候,需要判断该组件是否可用:
|
|
可以通过如下代码进入 启用/禁用 下载管理 界面:
|
|
本文意在讲解app的更新逻辑以及不同的表现形式的处理附带的介绍了使用nodejs写一个简单的api接口,重点是如何使用DownloadManager来实现apk的下载更新安装,顺带讲一下retrofit+rxjava的使用以及如何监听app是否下载完成。
DownloadManager的使用概括:
构建下载请求:
12 > new DownloadManager.Request(url)>
>
设置请求属性
12 > request.setXXX()>
>
调用downloadmanager对象的enqueue方法进行下载,此方法返回一个编号用于标示此下载任务:
1234 > downManager = (DownloadManager)getSystemService(Context.DOWNLOAD_SERVICE);>> id= downManager.enqueue(request);>
>
DownManager会对所有的现在任务进行保存管理,那么我们如何获取这些信息呢?这个时候就要用到DownManager.Query对象,通过此对象,我们可以查询所有下载任务信息。
setFilterById(long… ids):根据任务编号查询下载任务信息
setFilterByStatus(int flags):根据下载状态查询下载任务
如果想取消下载,则可以调用remove方法完成,此方法可以将下载任务和已经下载的文件同时删除:
12 > downManager.remove(id);>
好了具体的都讲的差不多了,本文以同步到我的csdn:安卓开发实战之app之版本更新升级(DownloadManager和http下载)完整实现
demo 传送门:AppUpdate.rar
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>Fragment系列文章:
1、Fragment全解析系列(一):那些年踩过的坑
2、Fragment全解析系列(二):正确的使用姿势
3、Fragment之我的解决方案:Fragmentation
如果你通读了本系列的前两篇,我相信你可以写出大部分场景都能正常运行的Fragment了。如果你想了解更多,那么你可以看看我封装的这个库:Fragmentation。
本篇主要介绍这个库,解决了一些BUG,使用简单,提供实时查看栈视图等实用功能。
源码地址:Github,欢迎Star,Fork。
Demo网盘下载
Demo演示:
单Activity + 多Fragment,项目中有3个Demo。
流式的单Activity+多Fragment:
类似微信交互方式的单Activity+多Fragment:(全页面支持滑动返回)
类似新版仿知乎交互方式的单Activity+多Frgment
为”单Activity + 多Fragment的架构”,”多模块Activity + 多Fragment的架构”而生,帮你简化使用过程,轻松解决各种复杂嵌套等问题,修复了官方Fragment库存在的一些BUG。
1、有效解决各种复杂嵌套、同级等Fragment重叠问题
2、实时查看Fragment的(包括嵌套Fragment)栈视图的对话框和Log,方便调试
3、增加启动模式、startForResult等类似Activity的方法
4、类似Android事件分发机制的Fragment回退方法:onBackPressedSupport(),轻松为每个Fragment实现Back按键事件
5、完美的防抖动解决方案(防止用户点击速度过快,导致启动多个Fragment)
6、提供可轻松 设定Fragment转场动画 的解决方案
7、修复官方库里pop(tag/id)出栈多个Fragment时的一些BUG
8、支持SwipeBack滑动边缘退出(需要使用Fragmentation_SwipeBack库,详情README)
1、2个新demo: 仿知乎交互 + 仿微信交互的新Demo,展示复杂嵌套Fragment的交互场景
2、全新的Fragment恢复机制
3、更容易编写各种嵌套Fragment的代码
4、支持同级Fragment的处理
5、实验性支持SharedElement-Material过渡动画
6、全新的类似Android事件分发机制的onBackPressedSupport()
1. 项目下app的build.gradle中依赖:
|
|
2. Activity继承SupportActivity:
|
|
3. Fragment继承SupportFragment:
|
|
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>Fragment系列文章:
1、Fragment全解析系列(一):那些年踩过的坑
2、Fragment全解析系列(二):正确的使用姿势
3、Fragment之我的解决方案:Fragmentation
本篇主要介绍一些Fragment使用技巧。
Fragment是可以让你的app纵享丝滑的设计,如果你的app想在现在基础上性能大幅度提高,并且占用内存降低,同样的界面Activity占用内存比Fragment要多,响应速度Fragment比Activty在中低端手机上快了很多,甚至能达到好几倍!如果你的app当前或以后有移植平板等平台时,可以让你节省大量时间和精力。
简陋的目录
1、一些使用建议
2、add(), show(), hide(), replace()的那点事
3、关于FragmentManager你需要知道的
4、使用FragmentPagerAdapter+ViewPager的注意事项
5、Fragment事务和动画,你可能不知道的坑
6、是使用单Activity+多Fragment的架构,还是多模块Activity+多Fragment的架构?
作为一个稳定的app,从后台且回到前台,一定会在任何情况都能恢复到离开前的页面,并且保证数据的完整性。
如果你没看过本系列的第一篇,为了方便后面文章的介绍,先规定一个“术语”,安卓app有一种特殊情况,就是 app运行在后台的时候,系统资源紧张的时候导致把app的资源全部回收(杀死app的进程),这时把app再从后台返回到前台时,app会重启。这种情况下文简称为:“内存重启”。(屏幕旋转等配置变化也会造成当前Activity重启,本质与“内存重启”类似)
1、对Fragment传递数据,建议使用setArguments(Bundle args)
,而后在onCreate
中使用getArguments()
取出,在 “内存重启”前,系统会帮你保存数据,不会造成数据的丢失。和Activity的Intent原理一致。
2、使用newInstance(参数)
创建Fragment对象,优点是调用者只需要关系传递的哪些数据,而无需关心传递数据的Key是什么。
3、如果你需要在Fragment中用到宿主Activity对象,建议在你的基类Fragment定义一个Activity的全局变量,在onAttach
中初始化,这不是最好的解决办法,但这可以有效避免一些意外Crash。详细原因参考第一篇的“getActivity()空指针”部分。
|
|
1、区别
show()
,hide()
最终是让Fragment的View setVisibility
(true还是false),不会调用生命周期;replace()
的话会销毁视图,即调用onDestoryView、onCreateView等一系列生命周期;
add()
和 replace()
不要在同一个阶级的FragmentManager里混搭使用。
2、使用场景
如果你有一个很高的概率会再次使用当前的Fragment,建议使用show()
,hide()
,可以提高性能。
在我使用Fragment过程中,大部分情况下都是用show()
,hide()
,而不是replace()
。
注意:如果你的app有大量图片,这时更好的方式可能是replace,配合你的图片框架在Fragment视图销毁时,回收其图片所占的内存。
3、onHiddenChanged的回调时机
当使用add()
+show(),hide()
跳转新的Fragment时,旧的Fragment回调onHiddenChanged()
,不会回调onStop()
等生命周期方法,而新的Fragment在创建时是不会回调onHiddenChanged()
,这点要切记。
4、Fragment重叠问题
使用show()
,hide()
带来的一个问题就是,如果你不做任何处理,在“内存重启”后,Fragment会重叠;
有些小伙伴可能就是为了避免Fragment重叠问题,而选择使用replace()
,但是使用show()
,hide()
时,重叠问题是完全可以解决的,有两种方式解决,详情参考上一篇。
1、FragmentManager栈视图
(1)每个Fragment以及宿主Activity(继承自FragmentActivity)都会在创建时,初始化一个FragmentManager对象,处理好Fragment嵌套问题的关键,就是理清这些不同阶级的栈视图。
下面给出一个简要的关系图:
(2)对于宿主Activity,getSupportFragmentManager()
获取的FragmentActivity的FragmentManager对象;
对于Fragment,getFragmentManager()
是获取的是父Fragment(如果没有,则是FragmentActivity)的FragmentManager对象,而getChildFragmentManager()
是获取自己的FragmentManager对象。
2、恢复Fragment时(同时防止Fragment重叠),选择getFragments()还是findFragmentByTag()
6月12日更:可以直接跳过,看我的解决方案,9行代码解决所有情况的Fragment重叠,传送门
(1)选择getFragments()
对于一个Activity内的多个Fragment,如果Fragment的关系是“流程”,比如登录->注册/忘记密码->填写信息->跳转到主页Activity。这种情况下,用getFragments()的方式是最合适的,在你的Activity内(更好的方式是在你的所有”流程”基类Activity里),写下如下代码:
|
|
上面恢复Fragment的方式,不仅提高性能,同时避免了Fragment重叠现象,最重要的事,你根本不用关心Activity容器里都有哪些Fragment。
(2)选择findFragmentByTag()
恢复
如果你的Activity的Fragments,不是“流程”关系,而是“同级”关系,比如QQ的主界面,“消息”、“联系人”、“动态”,这3个Fragment属于同级关系,用上面的代码就不合适了,恢复的时候总会恢复最后一个,即“动态Fragment”。
正确的做法是在onSaveInstanceState()
内保存当前所在Fragment的tag或者下标,在onCreate()
是恢复的时候,隐藏其它2个Fragment。
|
|
当然在“同级”关系中,使用getFragments()恢复也是可以的。
6月12日更:我的解决方案,9行代码解决所有情况的Fragment重叠,传送门
1、使用FragmentPagerAdapter+ViewPager时,切换回上一个Fragment页面时(已经初始化完毕)
使用FragmentPagerAdapter+ViewPager时,切换回上一个Fragment页面时(已经初始化完毕),不会回调任何生命周期方法以及onHiddenChanged()
,只有setUserVisibleHint(boolean isVisibleToUser)
会被回调,所以如果你想进行一些懒加载,需要在这里处理。
2、在给ViewPager绑定FragmentPagerAdapter时,new FragmentPagerAdapter(fragmentManager)
的FragmentManager,一定要保证正确,如果ViewPager是Activity内的控件,则传递getSupportFragmentManager()
,如果是Fragment的控件中,则应该传递getChildFragmentManager()
。只要记住ViewPager内的Fragments是当前组件的子Fragment这个原则即可。
3、你不需要在“内存重启”的情况下,去恢复的Fragments,有FragmentPagerAdapter的存在,不需要你去做恢复工作。
1、如果你在使用
|
|
方法后,紧接着直接调用类似如下事务的方法,出栈动画还没完成就进行下一个事务方法,这会导致在内存重启后按返回键报错问题。
如果你设置了动画,这个异常你可能比较熟悉
|
|
正确的做法是使用主线程的Handler,将事务放到Runnable里运行。
|
|
2、给Fragment设定Fragment转场动画时,如果你的app有使用popStackBackxx(tag/id,flags)出栈多个Fragment时,应避免直接使用.setCustomAnimations(enter, exit, popEnter, popExit)
,需要配合onCreateAnimtation方法
将出栈动画临时取消,本系列最后一篇给出了我的解决方案,解决了该问题,有兴趣可以自行查看 :)
(注意:如果你想给下一个Fragment设置进栈动画和出栈动画,.setCustomAnimations(enter, exit)只能设置进栈动画,第二个参数并不是设置出栈动画;请使用.setCustomAnimations(enter, exit, popEnter, popExit),这个方法的第1个参数对应进栈动画,第4个参数对应出栈动画,所以是.setCustomAnimations(进栈动画, exit, popEnter, 出栈动画))
另外一提:谨慎使用popStackBack(String tag/int id,int flasg)
系列的方法,原因在上一篇中已经描述。
Tip:
如果你遇到Fragment的mNextAnim空指针的异常(通常是在你的Fragment被重启的情况下),那么你首先需要检查是否操作的Fragment是否为null;其次在你的Fragment转场动画还没结束时,你是否就执行了其他事务等方法;解决思路就是延迟一个动画时间再执行事务,或者临时将该Fragment设为无动画
单Activity+多Fragment:
一个app仅有一个Activity,界面皆是Frament,Activity作为app容器使用。
优点:性能高,速度最快。参考:新版知乎 、google系app
缺点:逻辑比较复杂,尤其当Fragment之间联动较多或者嵌套较深时,比较复杂。
多模块Activity+多Fragment:
一个模块用一个Activity,比如
1、登录注册流程:
LoginActivity + 登录Fragment + 注册Fragment + 填写信息Fragment + 忘记密码Fragment
2、或者常见的数据展示流程:
DataActivity + 数据列表Fragment + 数据详情Fragment + …
优点:速度快,相比较单Activity+多Fragment,更易维护。
我的观点:
权衡利弊,我认为多模块Activity+多Fragment是最合适的架构,开发起来不是很复杂,app的性能又很高效。
当然。Fragment只是官方提供的灵活组件,请优先遵从你的项目设计!真的特别复杂的界面,或者单个Activity就可以完成一个流程的界面,使用Activity可能是更好的方案。
如果你读完了第一篇和这篇文章,那么我相信你使用多模块Activity+多Fragment的架构所遇到的坑,大部分都应该能找到解决办法。
但是如果流程较为复杂,比如Fragment A需要启动一个新的Fragment B并且关闭当前A,或者A启动B,B在获取数据后,想在返回到A时把数据交给A(类似Activity的startActivityForResult
),又或者你保证在Fragment转场动画的情况下,使用pop(tag\id)
从栈内退出多个Fragment,或者你甚至想Fragment有一个类似Activity的SingleTask
启动模式,那么你可以参考下一篇,我的解决方案库,Fragmentation。它甚至提供了一个让你在开发时,可以随时查看所有阶级的栈视图的UI界面。
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>本篇主要介绍一些最常见的Fragment的坑以及官方Fragment库的那些自身的BUG,这些BUG在你深度使用时会遇到,比如Fragment嵌套时或者单Activity+多Fragment架构时遇到的坑。
如果想看较为实用的技巧,请直接看第二篇
Fragment是可以让你的app纵享丝滑的设计,如果你的app想在现在基础上性能大幅度提高,并且占用内存降低,同样的界面Activity占用内存比Fragment要多,响应速度Fragment比Activty在中低端手机上快了很多,甚至能达到好几倍!如果你的app当前或以后有移植平板等平台时,可以让你节省大量时间和精力。
简陋的目录
1、getActivity()空指针
2、Fragment重叠异常—–正确使用hide、show的姿势
3、Fragment嵌套的那些坑
4、未必靠谱的出栈方法remove()
5、多个Fragment同时出栈的那些深坑BUG
6、超级深坑 Fragment转场动画
最新版知乎,单Activity多Fragment的架构,响应可以说非常“丝滑”,非要说缺点的话,就是没有转场动画,并且转场会有类似闪屏现象。我猜测可能和Fragment转场动画的一些BUG有关。(这系列的最后一篇文章我会给出我的解决方案,可以自定义转场动画,并能在各种特殊情况下正常运行。)
但是!Fragment相比较Activity要难用很多,在多Fragment以及嵌套Fragment的情况下更是如此。
更重要的是Fragment的坑真的太多了,看Square公司的这篇文章吧,Square:从今天开始抛弃Fragment吧!
当然,不能说不再用Fragment,Fragment的这些坑都是有解决办法的,官方也在逐步修复一些BUG。
下面罗列一些,有常见的,也有极度隐蔽的一些坑,也是我在用单Activity多Fragment时遇到的坑,可能有更多坑可以挖掘…
在这之前为了方便后面文章的介绍,先规定一个“术语”,安卓app有一种特殊情况,就是 app运行在后台的时候,系统资源紧张的时候导致把app的资源全部回收(杀死app的进程),这时把app再从后台返回到前台时,app会重启。这种情况下文简称为:“内存重启”。(屏幕旋转等配置变化也会造成当前Activity重启,本质与“内存重启”类似)
在系统要把app回收之前,系统会把Activity的状态保存下来,Activity的FragmentManager负责把Activity中的Fragment保存起来。在“内存重启”后,Activity的恢复是从栈顶逐步恢复,Fragment会在宿主Activity的onCreate
方法调用后紧接着恢复(从onAttach
生命周期开始)。
可能你遇到过getActivity()返回null,或者平时运行完好的代码,在“内存重启”之后,调用getActivity()的地方却返回null,报了空指针异常。
大多数情况下的原因:你在调用了getActivity()时,当前的Fragment已经onDetach()
了宿主Activity。
比如:你在pop了Fragment之后,该Fragment的异步任务仍然在执行,并且在执行完成后调用了getActivity()方法,这样就会空指针。
解决办法:
更”安全”的方法:(对于Fragment已经onDetach这种情况,我们应该避免在这之后再去调用宿主Activity对象,比如取消这些异步任务,但我们的团队可能会有粗心大意的情况,所以下面给出的这个方案会保证安全)
在Fragment基类里设置一个Activity mActivity的全局变量,在onAttach(Activity activity)
里赋值,使用mActivity代替getActivity()
,保证Fragment即使在onDetach
后,仍持有Activity的引用(有引起内存泄露的风险,但是相比空指针闪退,这种做法“安全”些),即:
|
|
如果你add()
了几个Fragment,使用show()、hide()
方法控制,比如微信、QQ的底部tab等情景,如果你什么都不做的话,在“内存重启”后回到前台,app的这几个Fragment界面会重叠。
原因是FragmentManager帮我们管理Fragment,当发生“内存重启”,他会从栈底向栈顶的顺序一次性恢复Fragment;
但是因为没有保存Fragment的mHidden属性,默认为false,即show状态,所以所有Fragment都是以show的形式恢复,我们看到了界面重叠。
(如果是replace
,恢复形式和Activity一致,只有当你pop之后上一个Fragment才开始重新恢复,所有使用replace
不会造成重叠现象)
详细原因:从源码角度分析,为什么会发生Fragment重叠?
这里给出3个解决方案:
1、是大家比较熟悉的 findFragmentByTag:
即在add()
或者replace()
时绑定一个tag,一般我们是用fragment的类名作为tag,然后在发生“内存重启”时,通过findFragmentByTag
找到对应的Fragment,并hide()
需要隐藏的fragment。
下面是个标准恢复写法:
|
|
如果你想恢复到用户离开时的那个Fragment的界面,你还需要在onSaveInstanceState(Bundle outState)
里保存离开时的那个可见的tag或下标,在onCreate
“内存重启”代码块中,取出tag/下标,进行恢复。
2、使用getSupportFragmentManager().getFragments()恢复
通过getFragments()
可以获取到当前FragmentManager管理的栈内所有Fragment。
标准写法如下:
|
|
从代码看起来,这种方式比较复杂,但是这种方式在一些场景下比第一种方式更加简便有效。
我会在下一篇中介绍在不同场景下如果选择,何时用findFragmentByTag()
,何时用getFragments()
恢复。
顺便一提,有些小伙伴会用一种并不合适的方法恢复Fragment,虽然效果也能达到,但并不恰当。即:
|
|
如果仅仅为了找回栈内的Fragment,使用putFragment(bundle, key, fragment)
保存fragment,是完全没有必要的;因为FragmentManager在任何情况都会帮你存储Fragment,你要做的仅仅是在“内存重启”后,找回这些Fragment即可。
。
3、我的解决方案,9行代码解决所有情况的Fragment重叠:传送门
其实一些小伙伴遇到的很多嵌套的坑,大部分都是由于对嵌套的栈视图产生混乱,只要理清栈视图关系,做好恢复相关工作以及正确选择是使用getFragmentManager()
还是getChildFragmentManager()
就可以避免这些问题。
这部分内容是我们感觉Fragment非常难用的一个点,我会在下一篇中,详细介绍使用Fragment嵌套的一些技巧,以及如何清晰分析各个层级的栈视图。
附:startActivityForResult接收返回问题
在support 23.2.0以下的支持库中,对于在嵌套子Fragment的startActivityForResult ()
,会发现无论如何都不能在onActivityResult()
中接收到返回值,只有最顶层的父Fragment才能接收到,这是一个support v4库的一个BUG,不过在前两天发布的support 23.2.0库中,已经修复了该问题,嵌套的子Fragment也能正常接收到返回数据了!
如果你想让某一个Fragment出栈,使用remove()
在加入回退栈时并不靠谱。
如果你在add的同时将Fragment加入回退栈:addToBackStack(name)的情况下,它并不能真正将Fragment从栈内移除,如果你在2秒后(确保Fragment事务已经完成)打印getSupportFragmentManager().getFragments()
,会发现该Fragment依然存在,并且依然可以返回到被remove的Fragment,而且是空白页面。
如果你没有将Fragment加入回退栈,remove方法可以正常出栈。
如果你加入了回退栈,popBackStack()
系列方法才能真正出栈,这也就引入下一个深坑,popBackStack(String tag,int flags)
等系列方法的BUG。
在Fragment库中如下4个方法是有BUG的:
1、popBackStack(String tag,int flags)
2、popBackStack(int id,int flags)
3、popBackStackImmediate(String tag,int flags)
4、popBackStackImmediate(int id,int flags)
上面4个方法作用是,出栈到tag/id的fragment,即一次多个Fragment被出栈。
1、FragmentManager栈中管理fragment下标位置的数组ArrayList mAvailIndeices的BUG
下面的方法FragmentManagerImpl类方法,产生BUG的罪魁祸首是管理Fragment栈下标的mAvailIndeices
属性:
|
|
上面代码最终导致了栈内顺序不正确的问题,如下图:
上面的这个情况,会一次异常,一次正常。带来的问题就是“内存重启”后,各种异常甚至Crash。
我发现这BUG的时候,我也懵比了,幸好,stackoverflow上有大神给出了解决方案!hack FragmentManagerImpl
的mAvailIndices
,对其进行一次Collections.reverseOrder()
降序排序,保证栈内Fragment的index的正确。
|
|
使用方法就是通过popBackStackImmediate(tag/id)
多个Fragment后,调用
|
|
2、popBackStack的坑popBackStack
和popBackStackImmediate
的区别在于前者是加入到主线队列的末尾,等其它任务完成后才开始出栈,后者是立刻出栈。
如果你popBackStack
多个Fragment后,紧接着beginTransaction()
add新的一个Fragment,接着发生了“内存重启”后,你再执行popBackStack()
,app就会Crash,解决方案是postDelay出栈动画时间再执行其它事务,但是根据我的观察不是很稳定。
我的建议是:如果你想出栈多个Fragment,你应尽量使用popBackStackImmediate(tag/id)
,而不是popBackStack(tag/id)
,如果你想在出栈后,立刻beginTransaction()
开始一项事务,你应该把事务的代码post/postDelay到主线程的消息队列里,下一篇有详细描述。
如果你的Fragment没有转场动画,或者使用setCustomAnimations(enter, exit)
的话,那么上面的那些坑解决后,你可以愉快的玩耍了。
|
|
(注意:如果你想给下一个Fragment设置进栈动画和出栈动画,.setCustomAnimations(enter, exit)只能设置进栈动画,第二个参数并不是设置出栈动画;请使用.setCustomAnimations(enter, exit, popEnter, popExit),这个方法的第1个参数对应进栈动画,第4个参数对应出栈动画,所以是.setCustomAnimations(进栈动画, exit, popEnter, 出栈动画))
总结起来就是Fragment没有出栈动画的话,可以避免很多坑。
如果想让出栈动画运作正常的话,需要使用Fragment的onCreateAnimation
中控制动画。
|
|
但是用代价也是有的,你需要解决出栈动画带来的几个坑。
1、pop多个Fragment时转场动画 带来的问题
在使用 pop(tag/id)
出栈多个Fragment的这种情况下,将转场动画临时取消或者延迟一个动画的时间再去执行其他事务;
原因在于这种情景下,如果发生“内存重启”后,因为Fragment转场动画没结束时再执行其他方法,会导致Fragment状态不会被FragmentManager正常保存下来。
2、进入新的Fragment并立刻关闭当前Fragment 时的一些问题
(1)如果你想从当前Fragment进入一个新的Fragment,并且同时要关闭当前Fragment。由于数据结构是栈,所以正确做法是先pop
,再add
,但是转场动画会有覆盖的不正常现象,你需要特殊处理,不然会闪屏!
(2)Fragment的根布局要设置android:clickable = true
,原因是在pop
后又立刻add
新的Fragment时,在转场动画过程中,如果你的手速太快,在动画结束前你多点击了一下,上一个Fragment的可点击区域可能会在下一个Fragment上依然可用。
Tip:
如果你遇到Fragment的mNextAnim空指针的异常(通常是在你的Fragment被重启的情况下),那么你首先需要检查是否操作的Fragment是否为null;其次在你的Fragment转场动画还没结束时,你是否就执行了其他事务等方法;解决思路就是延迟一个动画时间再执行事务,或者临时将该Fragment设为无动画
看了上面的介绍,你可能会觉得Fragment有点可怕。
但是我想说,如果你只是浅度使用,比如一个Activity容器包含列表Fragment+详情Fragment这种简单情景下,不涉及到popBackStack/Immediate(tag/id)
这些的方法,还是比较轻松使用的,出现的问题,网上都可以找到解决方案。
但是如果你的Fragment逻辑比较复杂,有特殊需求,或者你的app架构是仅有一个Activity + 多个Fragment,上面说的这些坑,你都应该全部解决。
在下一篇中,介绍了一些非常实用的使用技巧,包括如何解决Fragment嵌套、各种环境、组件下Fragment的使用等技巧,推荐阅读!
还有一些比较隐蔽的问题,不影响app的正常运行,仅仅是一些显示的BUG,并没有在上面介绍,在本系列的最后一篇,我给出了我的解决方案,一个我封装的Fragmentation库,解决了所有动画问题,非常适合单Activity+多Fragment 或者 多模块Activity+多Fragment的架构。有兴趣的可以看看 :)
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>防抖动:防止用户手抖,连续快速地点击多次同一个按钮。
在Fragment(v4)之间的跳转过程中,下面两种情况会导致用户体验不佳:(以AFragment 跳转 BFragment为例)
1、BFragment还没真正创建时(因为Fragment事务并不是立即开始的),你手抖点了2下,就会一次性启动2个BFragment;
2、在跳转BFragment过程中,如果有转场动画的存在,在动画结束前的任意时间,你点击了BFragment页面中可点击的区域,就会触发该点击区域的事件。
不做任何防手抖处理的话,你可能遭遇下面GIF的情况:
无防手抖处理
所以对Fragment进行防手抖处理还是很有必要的,RxJava系列的Rxbinding包可以帮你解决该问题,debounce操作符可以实现防手抖,但很多情况,你会使用RxJava但不想使用Rxbinding。
其实不管哪种方案,我想你都不会愿意在每个点击事件里都手动处理一次防抖动问题。
我们需要一个封装的基类,可以解决防抖动问题,而不必在每个启动Fragment的按钮都进行防抖动处理。
有1种常见的防手抖处理方式:通过时间差
|
|
这种方式虽然也能实现防抖动的效果,但是适用性有限;
比如如果我需要add一个Fragment,然后紧接着该Fragment又立即需要add一个子Fragment,这时add子Fragment的操作可能就会被这个时间差屏蔽掉。
鉴于上述方法的拘束性,再结合Fragment的宿主是Activity,我有了下面的方案:
利用Activity的dispatchTouchEvent(MotionEvent ev)
方法。
大致思路是,在点击后立即让Activity拦截一切Touch事件,在目标Fragment的转场动画结束后(如果是无转场动画,则是在onActivityCreated被调用后),再让Activity取消拦截。
接下来我们就看看具体如何实现吧!
|
|
所有Activity中的Touch事件,首先都需要经过Activity的dispatchTouchEvent
方法,该方法通过Window分发Touch事件,如果返回true,则不再往下层分发,这时我们布局内的任何Touch事件都会“无效”。
在通过Activity来加载Fragment时,将mAllowClickable设为false,此时只到为true时,屏幕的Touch事件将都无效。
这里将有转场动画和无转场动画两种情况都进行了防手抖处理:
|
|
在Fragment初始化时,其生命周期顺序:
… -> onCreateView -> onCreateAnimation -> onActivityCreated
由此生命周期,再结合代码上的注释,相信你一看就懂了~
最终,利用Android的事件分发机制,使用不到50行代码就完美解决了Fragment的防手抖处理。
最后看下效果:
防手抖
想查看更多Fragment优化细节或者深度使用Fragment的小伙伴,建议看看我的这个Fragmentation库
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>在上一篇从源码角度分析,为什么会发生Fragment重叠?里,我们分析了造成Fragment重叠的原因,这一篇我会介绍几个解决方案,同时给出一个我的方案:9行代码让你app内的Fragment对重叠说再见!
这两种方案在我这篇简书Fragment全解析系列(一):那些年踩过的坑里有比较详细的介绍,可以自行查看,这里不多做介绍,这两种方案也是最常见的解决方案。
缺点:
下面这个方案,是我在完善Fragmentation库时想到的方案。
Fragmentation库前几天push了改变重大的0.7版本,应该算是比较成熟了,如果你重度使用Fragment或者想使用单Activity+多Fragment的组件架构的话,强烈推荐看看;对于各种复杂嵌套、同级Fragment的使用场景,你不必担心Fragment的重叠,同时大大简化Fragment的嵌套逻辑。
在上一篇分析中,我们知道了发生Fragment重叠的根本原因在于FragmentState没有保存Fragment的显示状态,即mHidden
,导致页面重启后,该值为默认的false,即show状态,所以导致了Fragment的重叠。
根据这个原因,我想到我们手动维护一个mSupportHidden
不就行了吗?
看下面的基类Fragment代码:
|
|
是的你没看错,只要上面的9行代码! FragmentState没帮我们保存Hidden状态,那就我们自己来保存,在页面重启后,我们自己来决定Fragment是否显示!
解决思路转变了,由Activity/父Fragment来管理子Fragment的Hidden状态转变为 由Fragment自己来管理自己的Hidden状态!
优点:不管多深的嵌套Fragment、同级Fragment等场景,全都可以正常工作,不会发生重叠!
缺点:暂时没有发现。
补充:
有些小伙伴反应还是会重叠,其实是因为加载根Fragment时没有经过判断的原因,当在类似onCreate
等初始化生命周期里加载根Fragment(即第一个Fragment)时,需要下面的判断,避免重复加载相同的Fragment:
|
|
最后的方案用在了我的Fragmentation库中,在新的仿知乎的Demo里,各种复杂场景表现完美。
但是这个方案真的神奇的不可思议,在我的测试下,各种情况都正常适用,但是之前从没看到有人提到过该方案,所以如果你发现该方案有我没有考虑到的BUG,请第一时间告诉我!
最后是一点心得体会:
当遇到问题时,我们如果从源头思考:为什么会发生这个问题? 从源码角度分析问题,可能就会得到一个更好的解决问题的思路!
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>我们在使用Fragment的过程中,有时会发现一直表现正常的Fragment,突然重叠了!
一般满足下面2个条件才可能会发生重叠:
1、发生了页面重启(旋转屏幕、内存不足等情况被强杀重启)。
2、重复replace
|add
Fragment 或者 使用show
, hide
控制Fragment;
我们知道Activity中有个onSaveInstanceState()
方法,该方法在app进入后台、屏幕旋转前、跳转下一个Activity等情况下会被调用,此时系统帮我们保存一个Bundle类型的数据,我们可以根据自己的需求,手动保存一些例如播放进度等数据,而后如果发生了页面重启,我们可以在onRestoreInstanceState()
或onCreate()
里get该数据,从而恢复播放进度等状态。
而产生Fragment重叠的原因就与这个保存状态的机制有关,大致原因就是系统在页面重启前,帮我们保存了Fragment的状态,但是在重启后恢复时,视图的可见状态没帮我们保存,而Fragment默认的是show状态,所以产生了Fragment重叠现象。
分析:
我们先看FragmentActivity的相关源码:
|
|
从上面源码可以看出,FragmentActivity确实是帮我们保存了Fragment的状态,并且在页面重启后会帮我们恢复!
其中的mFragments
是FragmentController,它是一个Controller,内部通过FragmentHostCallback间接控制FragmentManagerImpl。
相关代码如下:
|
|
通过上面代码可以看出FragmentController通过FragmentHostCallback里的FragmentManagerImpl对象来控制恢复工作。
我们接着看FragmentManagerImpl到底做了什么:
|
|
我们通过saveAllState()
看到了关键的保存代码,原来是是通过FragmentManagerState来保存Fragment的状态、所处Fragment栈下标、回退栈状态等。
而在restoreAllState()
恢复时,通过FragmentManagerState里的FragmentState的instantiate()
方法恢复了Fragment(见下面的分析就明白啦)
我们看下FragmentManagerState:
|
|
我们只看FragmentState,它也实现了Parcelable,保存了Fragment的类名、下标、id、Tag、ContainerId以及Arguments等数据:
|
|
至此,我们就明白了系统帮我们保存的Fragment其实最终是以FragmentState形式存在的。
此时我们再思考下为什么在页面重启后会发生Fragment的重叠? 其实答案已经很明显了,根据上面的源码分析,我们会发现FragmentState里没有Hidden状态的字段!
而Hidden状态对应Fragment中的mHidden
,该值默认false…
|
|
我想你应该明白了,在以add方式加载Fragment的场景下,系统在恢复Fragment时,mHidden=false
,即show状态,这样在页面重启后,Activity内的Fragment都是以show状态显示的,而如果你不进行处理,那么就会发生Fragment重叠现象!
replace
|add
Fragment 或者 使用show
, hide
控制Fragment会导致重叠?重复replace|add Fragment
我们知道加载Fragment有2种方式:replace()
和add()
。
不管哪种方式,重复加载Fragment都会导致重叠,这个很好理解,你加载同一个Fragment2次当然会重叠了;问题是我们在哪里重复加载了Fragment?
一般情况下,我们会在Activity的onCreate()
里或者Fragment的onCreateView()
里加载根Fragment,如果在这里没有进行页面重启的判断的话,就可能导致重复加载Fragment引起重叠,正确的写法应该是:
|
|
这里一定要在saveInstanceState==null
时才加载Fragment,因为经过上面的分析,在页面重启时,Fragment的状态会被保存恢复,而此时再加载Fragment会重复加载,就导致栈已经有该Fragment的情况下,再加载一该Fragment,从而导致重叠!
使用show , hide控制Fragment
我们使用show()
,hide()
时,都是使用add
的方式加载Fragment的,add配合hide使Fragment的视图改变为GONE状态;而replace是销毁Fragment 的视图。
页面重启时,add的Fragment会全部走生命周期,创建视图;而replace的非栈顶Fragment不会走生命周期,只有Back时,才会逐一走栈顶Fragment生命周期,创建视图。
结合上面的源码分析,在使用replace加载Fragment时,页面重启后,Fragment视图都还没创建,所以mHidden
没有意义,不会发生重叠现象;
而在使用add加载时,视图是存在的并且叠加在一起,页面重启后 mHidden=false
,所有的Fragment都会是show状态显示出来(即VISIBLE),从而造成了Fragment重叠!
通过上面的分析,我想小伙伴们应该彻底明白Fragment重叠的原因了吧!
鉴于篇幅原因,我另写了一篇简书来谈谈 Fragment重叠的解决方案,同时会给出我通过分析源码想到的一个解决方案,下一篇解决方案的传送门
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>现在的Android应用程序中,不可避免的都会使用到图片,如果每次加载图片的时候都要从网络重新拉取,这样不但很耗费用户的流量,而且图片加载的也会很慢,用户体验很不好。所以一个应用的图片缓存策略是很重要的。通常情况下,Android应用程序中图片的缓存策略采用“内存-本地-网络”三级缓存策略。
当然现在处理网络图片的时候,一般人都会选择UIL或Glide,它已经将网络缓存处理的相当好了,并且glide还能加载gif,面试的时候通常也会问到图片缓存是怎么做的,这里就来简单实现这个功能。
首次加载 Android App 时,肯定要通过网络交互来获取图片,之后我们可以将图片保存至本地SD卡和内存中
之后运行 App 时,优先访问内存中的图片缓存,若内存中没有,则加载本地SD卡中的图片
总之,只在初次访问新内容时,才通过网络获取图片资源
内存缓存说白了就是在内存中保存一份图片集合,首先会想到HashMap这种键值对的形式来进行保存,以url作为key,bitmap作为value。
HashMap
键值对的方式保存图片,key为地址,value为图片对象,但因是强引用对象,很容易造成内存溢出,可以尝试SoftReference软引用对象HashMap>
SoftReference 为软引用对象(GC垃圾回收会自动回收软引用对象),但在Android2.3+后,系统会优先考虑回收弱引用对象,官方提出使用LruCache通过 LruCache
least recentlly use 最少最近使用算法
会将内存控制在一定的大小内, 超出最大值时会自动回收, 这个最大值开发者自己定
所以呢这里使用LruCache来进行:
这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
|
|
从网络加载完图片之后,将图片保存到本地SD卡中。在加载图片的时候,判断一下SD卡中是否有图片缓存,如果有,就直接从SD卡加载图片。本地缓存的工具类中有两个公共的方法,分别是向本地SD卡设置网络图片,获取SD卡中的图片。设置图片的时候采用键值对的形式进行存储,将图片的url作为键,作为文件的名字,图片的Bitmap作位值来保存。由于url含有特殊字符,不能直接作为图片的名字来存储,故采用url的MD5值作为文件的名字。
在初次通过网络获取图片后,我们可以在本地SD卡中将图片保存起来
可以使用MD5加密图片的网络地址,来作为图片的名称保存
|
|
|
|
网络拉取图片严格来讲不能称之为缓存,实质上就是下载url对应的图片,我们这里姑且把它看作是缓存的一种。
|
|
严格的来说需要提供一个门面来加载显示图片的,这里就简单的加载了:
|
|
从代码里我们可以看出,显示我们就是按顺序判断是否在内存中有bitmap,有就显示,没有就继续从本地找bitmap都没有去网络下载显示。
上述代码里面注释了DiskCache,因为自己写的一般保存到本地的方法都为存文件到sd卡,这里就顺带介绍下DiskLruCache。
从各大图片缓存框架我们知道从网络上获取到之后都会存入到本地缓存中使用的是DiskLruCache,
由于DiskLruCache并不是由Google官方编写的,所以这个类并没有被包含在Android API当中,我们需要将这个类从网上下载下来,然后手动添加到项目当中。(代码有点多)
|
|
首先你要知道,DiskLruCache是不能new出实例的,如果我们要创建一个DiskLruCache的实例,则需要调用它的open()方法,接口如下所示:
|
|
|
|
好了缓存代码都手把手的写完了,使用很简单
|
|
就一句话ImageLoder(this).disPlayImage(iv,”http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg“);
好了,demo就不上传了,所有的代码附上面了,看下效果吧:
当然别忘了添加所需权限:
|
|
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>在操作系统中,线程是操作系统调度的最小单元,当系统中存在大量线程时,系统会通过时间片轮转的方式调度每个线程,当进程中频繁的创建和销毁线程的时候应该采用线程池,线程池会缓存一定数量的线程,避免频繁创建和销毁线程带来的系统开销。
(1) 在Java中默认情况下一个进程只有一个线程,也就是主线程,其他线程都是子线程,也叫工作线程。Android中的主线程主要处理和界面相关的事情,而子线程则往往用于执行耗时操作。线程的创建和销毁的开销较大,所以如果一个进程要频繁地创建和销毁线程的话,都会采用线程池的方式。
(2) 在Android中除了Thread,还有HandlerThread、AsyncTask以及IntentService等也都扮演着线程的角色,只是它们具有不同的特性和使用场景。AsyncTask封装了线程池和Handler,它主要是为了方便开发者在子线程中更新UI。HandlerThread是一种具有消息循环的线程,在它的内部可以使用Handler。IntentService是一个服务,它内部采用HandlerThread来执行任务,当任务执行完毕后就会自动退出。因为它是服务的缘故,所以和后台线程相比,它比较不容易被系统杀死。
(3). 从Android 3.0开始,系统要求网络访问必须在子线程中进行,否则网络访问将会失败并抛出NetworkOnMainThreadException这个异常,这样做是为了避免主线程由于被耗时操作所阻塞从而出现ANR现象。
AsyncTask是一个抽象泛型类,它提供了Params、Progress、Result三个泛型参数,如果task确实不需要传递具体的参数,那么都可以设置为Void。下面是它的四个核心方法,其中doInBackground不是在主线程执行的。
onPreExecute、doInBackground、onProgressUpdate、onPostResult
它继承了Thread,是一种可以使用Handler的Thread.他的实现也很简单,就是在run方法中通过Looper。prepare来创建消息队列,并通过Looper.loop来开启消息循环,这样在使用的时候就可以在HandlerThread中创建Handler了。
|
|
HandlerThread在内部创建了消息队列,外界需要通过Handler的消息方式开通知它执行一个具体的任务,它的一个具体实现是IntentService。当我们不需要使用Handler的时候可以通过quit获取quitSafelt方法终结线程的执行。
继承了Service是一个抽象类,必须创建它的子类才可以使用它,IntentService可以执行后台耗时任务,当任务执行完毕之后会自动停止,因为他是服务,所以他的优先级比普通的线程高,比较适合执行一些高优先级的后台任务。
Android里面,耗时的网络操作,都会开子线程,在程序里面直接开过多的线程会消耗过多的资源,在众多的开源框架中也总能看到线程池的踪影,所以线程池是必须要会把握的一个知识点;
cpu资源
时间片
,系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行,因为时间相当短,多个线程频繁地发生切换,因此给用户的感觉就是好像多个线程同时运行一样,但是如果计算机有多个CPU,线程就能真正意义上的同时运行了.减少频繁的创建和销毁对象。
Executors:jdk1.5之后的一个新类,
提供了一些静态方法,帮助我们方便的生成一些常用的线程池
,ThreadPoolExecutor是Executors类的底层实现
1.newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行>所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池>保证所有任务的执行顺序按照任务的提交顺序执行。
2.newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3.newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
4.newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
|
|
corePoolSize:核心池的大小
,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
maximumPoolSize:’线程池最大线程数
,
这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止
。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
unit:参数keepAliveTime的时间单位
,有7种取值
|
|
workQueue : 任务队列
,是一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,参考BlockingQueue
|
|
threadFactory : 线程工厂
,如何去创建线程的
handler : 任务队列添加异常的捕捉器
,参考 RejectedExecutionHandler
|
|
阻塞队列,如果BlockingQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态,直到BlockingQueue进了东西才会被唤醒,同样,如果BlockingQueue是满的,任何试图往里存东西的操作也会被阻断进入等待状态,直到BlockingQueue里有空间时才会被唤醒继续操作。
ArrayBlockingQueue(有界队列)
: FIFO 队列,规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小LinkedBlockingQueue(无界队列)
:FIFO 队列,大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定。PriorityBlockingQueue
:优先级队列, 类似于LinkedBlockingQueue,但队列中元素非 FIFO, 依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序SynchronousQueue(直接提交策略)
: 交替队列,队列中操作时必须是先放进去,接着取出来
,交替着去处理元素的添加和移除,这是一个很有意思的阻塞队列,其中每个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。因此此队列内部其 实没有任何一个元素,或者说容量是0,严格说并不是一种容器。由于队列没有容量,因此不能调用peek操作,因为只有移除元素时才有元素。未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>通过上述分析,我们知道了通过new Thread().start()方式创建线程去处理任务的弊端,而为了解决这些问题,Java为我们提供了ExecutorService线程池来优化和管理线程的使用
1、线程的创建和销毁由线程池维护,一个线程在完成任务后并不会立即销毁,而是由后续的任务复用这个线程,从而减少线程的创建和销毁,节约系统的开销
2、线程池旨在线程的复用,这就可以节约我们用以往的方式创建线程和销毁所消耗的时间,减少线程频繁调度的开销,从而节约系统资源,提高系统吞吐量
3、在执行大量异步任务时提高了性能
4、Java内置的一套ExecutorService线程池相关的api,可以更方便的控制线程的最大并发数、线程的定时任务、单线程的顺序执行等
通常来说我们说到线程池第一时间想到的就是它:ExecutorService,它是一个接口,其实如果要从真正意义上来说,它可以叫做线程池的服务,因为它提供了众多接口api来控制线程池中的线程,而真正意义上的线程池就是:ThreadPoolExecutor,它实现了ExecutorService接口,并封装了一系列的api使得它具有线程池的特性,其中包括工作队列、核心线程数、最大线程数等。
既然线程池就是ThreadPoolExecutor,所以我们要创建一个线程池只需要new ThreadPoolExecutor(…);就可以创建一个线程池,而如果这样创建线程池的话,我们需要配置一堆东西,非常麻烦,我们可以看一下它的构造方法就知道了:
|
|
所以,官方也不推荐使用这种方法来创建线程池,而是推荐使用Executors的工厂方法来创建线程池,Executors类是官方提供的一个工厂类,它里面封装好了众多功能不一样的线程池,从而使得我们创建线程池非常的简便,主要提供了如下五种功能不一样的线程池:
1、newFixedThreadPool() :
作用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。
栗子:假如有一个新任务提交时,线程池中如果有空闲的线程则立即使用空闲线程来处理任务,如果没有,则会把这个新任务存在一个任务队列中,一旦有线程空闲了,则按FIFO方式处理任务队列中的任务。2、newCachedThreadPool() :
作用:该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。
栗子:假如该线程池中的所有线程都正在工作,而此时有新任务提交,那么将会创建新的线程去处理该任务,而此时假如之前有一些线程完成了任务,现在又有新任务提交,那么将不会创建新线程去处理,而是复用空闲的线程去处理新任务。那么此时有人有疑问了,那这样来说该线程池的线程岂不是会越集越多?其实并不会,因为线程池中的线程都有一个“保持活动时间”的参数,通过配置它,如果线程池中的空闲线程的空闲时间超过该“保存活动时间”则立刻停止该线程,而该线程池默认的“保持活动时间”为60s。3、newSingleThreadExecutor() :
作用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。4、newScheduledThreadPool() :
作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。5、newSingleThreadScheduledExecutor() :
作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面的区别是该线程池大小为1,而上面的可以指定线程池的大小。
好了,写了一堆来介绍这五种线程池的作用,接下来就是获取这五种线程池,通过Executors的工厂方法来获取:
|
|
Executors 提供四种线程池:
我们可以看到通过Executors的工厂方法来创建线程池极其简便,其实它的内部还是通过new ThreadPoolExecutor(…)的方式创建线程池的,我们看一下这些工厂方法的内部实现:
|
|
我们可以清楚的看到这些方法的内部实现都是通过创建一个ThreadPoolExecutor对象来创建的,正所谓万变不离其宗,所以我们要了解线程池还是得了解ThreadPoolExecutor这个线程池类,其中由于和定时任务相关的线程池比较特殊(newScheduledThreadPool()、newSingleThreadScheduledExecutor()),它们创建的线程池内部实现是由ScheduledThreadPoolExecutor这个类实现的,而ScheduledThreadPoolExecutor是继承于ThreadPoolExecutor扩展而成的,所以本质还是一样的,只不过多封装了一些定时任务相关的api
Java里面线程池的顶级接口是 Executor,不过真正的线程池接口是 ExecutorService, ExecutorService 的默认实现是 ThreadPoolExecutor;普通类 Executors 里面调用的就是 ThreadPoolExecutor。
照例看一下各个接口的源码:
|
|
下面我创建的一个线程池:
|
|
通过 ThreadPoolExecutor 的构造函数,撸一撸线程池相关参数的概念:
|
|
BlockingQueue
对象,而泛型则限定它是用来存放Runnable对象的,刚刚上面讲了,不同的线程池它的任务队列实现肯定是不一样的,所以,保证不同线程池有着不同的功能的核心就是这个workQueue的实现了,细心的会发现在刚刚的用来创建线程池的工厂方法中,针对不同的线程池传入的workQueue也不一样,下面我总结一下这五种线程池分别用的是什么BlockingQueue:1、newFixedThreadPool()—>LinkedBlockingQueue
2、newSingleThreadExecutor()—>LinkedBlockingQueue
3、newCachedThreadPool()—>SynchronousQueue
4、newScheduledThreadPool()—>DelayedWorkQueue
5、newSingleThreadScheduledExecutor()—>DelayedWorkQueue
这些队列分别表示:
LinkedBlockingQueue:无界的队列
SynchronousQueue:直接提交的队列
DelayedWorkQueue:等待队列
当然实现了BlockingQueue接口的队列还有:ArrayBlockingQueue(有界的队列)、PriorityBlockingQueue(优先级队列)。这些队列的详细作用就不多介绍了。
线程池的关闭
ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是 shutdown() 和 shutdownNow()。
shutdown():不会立即的终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
使用线程池,其中涉及到一个极其重要的方法,即:
|
|
该方法意为执行给定的任务,该任务处理可能在新的线程、已入池的线程或者正调用的线程,这由ThreadPoolExecutor的实现决定。
newFixedThreadPool
创建一个固定线程数量的线程池,示例为:
|
|
上述代码,我们创建了一个线程数为3的固定线程数量的线程池,同理该线程池支持的线程最大并发数也是3,而我模拟了10个任务让它处理,执行的情况则是首先执行前三个任务,后面7个则依次进入任务队列进行等待,执行完前三个任务后,再通过FIFO的方式从任务队列中取任务执行,直到最后任务都执行完毕。
为了体现出线程的复用,我特地在Log中加上了当前线程的名称,效果为:
newSingleThreadExecutor
创建一个只有一个线程的线程池,每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待线程处理完再依次处理任务队列中的任务,示例为:
|
|
代码还是差不多,只不过改了线程池的实现方式,效果我想大家都知道,即依次一个一个的处理任务,而且都是复用一个线程,效果为:
其实我们通过newSingleThreadExecutor()和newFixedThreadPool()的方法发现,创建一个singleThreadExecutorPool实际上就是创建一个核心线程数和最大线程数都为1的fixedThreadPool。
newCachedThreadPool
创建一个可以根据实际情况调整线程池中线程的数量的线程池,示例为:
|
|
为了体现该线程池可以自动根据实现情况进行线程的重用,而不是一味的创建新的线程去处理任务,我设置了每隔1s去提交一个新任务,这个新任务执行的时间也是动态变化的,所以,效果为:
newScheduledThreadPool
创建一个可以定时或者周期性执行任务的线程池,示例为:
|
|
newSingleThreadScheduledExecutor
创建一个可以定时或者周期性执行任务的线程池,该线程池的线程数为1,示例为:
|
|
实际上这个和上面的没什么太大区别,只不过是线程池内线程数量的不同,效果为:
每隔2秒就会执行一次该任务
Java内置只为我们提供了五种常用的线程池,一般来说这足够用了,不过有时候我们也可以根据需求来自定义我们自己的线程池,而要自定义不同功能的线程池,上面我们也说了线程池功能的不同归根到底还是内部的BlockingQueue实现不同,所以,我们要实现我们自己相要的线程池,就必须从BlockingQueue的实现上做手脚,而上面也说了BlockingQueue的实现类有多个,那么这次我们就选用PriorityBlockingQueue来实现一个功能是按任务的优先级来处理的线程池。
|
|
|
|
|
|
我们看下刚刚自定义的线程池是否达到了我们想要的功能,即根据任务的优先级进行优先处理任务,效果如下:
可以从执行结果中看出,由于核心线程数设置为3,刚开始时,系统有3个空闲线程,所以无须使用任务队列,而是直接运行前三个任务,而后面再提交任务时由于当前没有空闲线程所以加入任务队列中进行等待,此时,由于我们的任务队列实现是由PriorityBlockingQueue实现的,所以进行等待的任务会经过优先级判断,优先级高的放在队列前面先处理。从效果图中也可以看到后面的任务是先执行优先级高的任务,然后依次递减。
从上面我们可以得知,创建一个优先级线程池非常有用,它可以在线程池中线程数量不足或系统资源紧张时,优先处理我们想要先处理的任务,而优先级低的则放到后面再处理,这极大改善了系统默认线程池以FIFO方式处理任务的不灵活
除了内置的功能外,ThreadPoolExecutor也向外提供了三个接口供我们自己扩展满足我们需求的线程池,这三个接口分别是:
beforeExecute() - 任务执行前执行的方法
afterExecute() -任务执行结束后执行的方法
terminated() -线程池关闭后执行的方法
这三个方法在ThreadPoolExecutor内部都没有实现
前面两个方法我们可以在ThreadPoolExecutor内部的runWorker()方法中找到,而runWorker()是ThreadPoolExecutor的内部类Worker实现的方法,Worker它实现了Runnable接口,也正是线程池内处理任务的工作线程,而Worker.runWorker()方法则是处理我们所提交的任务的方法,它会同时被多个线程访问,所以我们看runWorker()方法的实现,由于涉及到多个线程的异步调用,必然是需要使用锁来处理,而这里使用的是Lock来实现的,我们来看看runWorker()方法内主要实现:
可以看到在task.run()之前和之后分别调用了beforeExecute和afterExecute方法,并传入了我们的任务Runnable对象
而terminated()则是在关闭线程池的方法中调用,而关闭线程池有两个方法,我贴其中一个:
所以,我们要扩展线程池,只需要重写这三个方法,并实现我们自己的功能即可,这三个方法分别都会在任务执行前调用、任务执行完成后调用、线程池关闭后调用。
这里我验证一下,继承自ThreadPoolExecutor 并实现那三个方法:
|
|
而运行后的结果则是,这正符合刚刚说的:
|
|
所以,在上面我们的优先级线程池的代码上,我们再扩展一个具有暂停功能的优先级线程池,代码如下:
具有暂时功能的线程池:
|
|
然后结合上面的优先级线程池的实现,创建具有暂停功能的优先级线程池:
|
|
这里我为了演示效果,把这个线程池设为只有一个线程,然后直接在TextView中显示当前执行的任务的优先级,然后设置个开关,控制线程池的暂停与开始:
|
|
效果为:
从效果上来看,该线程池和优先级线程一样,而且还多了一个暂停与开始的功能
虽说线程池极大改善了系统的性能,不过创建线程池也是需要资源的,所以线程池内线程数量的大小也会影响系统的性能,大了反而浪费资源,小了反而影响系统的吞吐量,所以我们创建线程池需要把握一个度才能合理的发挥它的优点,通常来说我们要考虑的因素有CPU的数量、内存的大小、并发请求的数量等因素,按需调整。
通常核心线程数可以设为CPU数量+1,而最大线程数可以设为CPU的数量*2+1。
获取CPU数量的方法为:
|
|
关于线程池的停止,ExecutorService为我们提供了两个方法:shutdown和shutdownNow,这两个方法各有不同,可以根据实际需求方便的运用,如下:
1、shutdown()方法在终止前允许执行以前提交的任务。
2、shutdownNow()方法则是阻止正在任务队列中等待任务的启动并试图停止当前正在执行的任务。
大家都知道AsyncTask内部实现其实就是Thread+Handler。其中Handler是为了处理线程之间的通信,而这个Thread到底是指什么呢?通过AsyncTask源码可以得知,其实这个Thread是线程池,AsyncTask内部实现了两个线程池,分别是:串行线程池和固定线程数量的线程池。而这个固定线程数量则是通过CPU的数量决定的。
在默认情况下,我们大都通过AsyncTask::execute()来执行任务的,
,而execute()内部则是调用executeOnExecutor(sDefaultExecutor, params)方法执行的,第一个参数就是指定处理该任务的线程池,而默认情况下AsyncTask是传入串行线程池(在这里不讲版本的变化),也就是任务只能单个的按顺序执行,而我们要是想让AsyncTask并行的处理任务,大家都知道调用AsyncTask::executeOnExecutor(sDefaultExecutor, params)方法传入这个参数即可:AsyncTask.THREAD_POOL_EXECUTOR。
而这个参数的意义在于为任务指定了一个固定线程数量的线程池去处理,从而达到了并行处理的功能,我们可以在源码中看到AsyncTask.THREAD_POOL_EXECUTOR这个参数就是一个固定线程数量的线程池:
|
|
1)什么是 Executor 框架?
Executor框架在Java 5中被引入,Executor 框架是一个根据一组执行策略调用、调度、执行和控制的异步任务的框架。
无限制的创建线程会引起应用程序内存溢出,所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executor 框架可以非常方便的创建一个线程池。
2)Executors 类是什么?
Executors为Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类提供了一些工具方法。Executors 可以用于方便的创建线程池。
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>Android的并发编程,即多线程开发,而Android的多线程开发模型也是源于Java中的多线程模型。
所以本篇也会先讲一些Java中的多线程理念,再讲解具体涉及的类,最后深入Android中的并发场景和实践。
举个很简单的栗子,当你一边在撸撸撸,一边在看小视频,同时在做两件事,这就是并发。
咳,年轻人节制啊。
并发的好处
提高资源利用率
当一个任务并没有完全占用系统资源,就可以利用并发来提高资源利用率,同时也能更快地完成任务。
当你的右手在干些什么的时候,左手是不是在没事做呢?那就也用起来呗。
(某次聚会,一名骑马的汉子说自己左右互搏(lu)特厉害)。
在程序任务上更加精简
就拿上一个栗子来说,左手做什么,右手做什么,任务明确分配好,又能同时进行,既提高了效率,逻辑又清晰。
更好的响应程序
这个拿Android客户端举个栗子,上传图片时,当前界面还是正常运转没有卡死,图片也正常上传,既保证了界面被响应,又保证图片可以上传。
### 并发的代价
需要占用更多的资源。
设计好一个并发程序并不容易。
并发的资源交互问题复杂。
并发的隐患
滥用资源导致系统不稳定
结果与预期不符
出现BUG难以排查
就拿Android中的App来说,一般来说一个app就是一个进程,(除了特殊的手段开启了多个进程,这里不深入这个话题,就是一个一对多的关系)。
线程是什么
进程只是一个程序、任务的统称,但是却不能执行任务,真正执行任务的是线程,所以线程是由进程创建的,一个进程可以创建多个线程。
线程可以调度资源等等,在这里只需要了解大致的概念就好,如果要深入可以学习一下操作系统。
人脑就相当于是CPU,想做一件事的时候,这个任务就是一个进程了,需要运用手脚等器官去完成这个任务,而手脚器官就可以理解成一个个线程,去做了不同的事,从而完成任务。
还是用Android举栗子,当你在手机上操作的时候,这个被称之为UI线程(之后会详解)。而一个最基本的app,不需要复杂的功能时,就只有一个UI线程和我们交互,那么这个app就是个单线程的。一般的程序面向用户的线程就是UI线程,也称之为主线程,单线程程序,其实就是只有一个主线程的程序。
多个进程可以算是并发,但是我们所说的并发场景,大部分是在一个进程中的,而并发就是由线程完成的,多个线程同时执行任务,就称之为并发。
以下为多线程工作示意图:
A线程要写文件C,B线程也要写文件C,这个时候就好像你拿着两只笔同时往纸上写东西,写出来的是什么自己也不知道。
这个时候我们需要一个类似于锁的东西,当C被A在写的时候,B不能写,B要等A写完了才能继续写。
至于这个锁到底是什么会在后面继续讲到。
死锁的四个条件是:
预防死锁就是至少破坏这四个条件其中一项,即破坏“禁止抢占”、破坏“持有等待”、破坏“资源互斥”和破坏“循环等待”。
举个例子:
A在B那边割包皮,B把A割坏了,A占着B的床位,要B赔钱,B要A让出床位才给钱。双方僵持不下。
在Java中,线程通常就是指Thread这个类,或者实现了Runnable的类,其实Thread这个类也是实现了Runnable接口的,可以看一下Runnable接口的代码:
里面就是一个run方法需要被实现。
再看一下Thread类的声明:
确实是一个实现了Runnable的类。
那么Thread类中拥有start()方法,和run()方法,下面用run()方法直接调用
得到信息:
发现其实和外面的线程是在同一个线程上。
而调用start()方法得到的信息是:
发现线程名不一样了,用start会开启一个新的线程,而run还是在当前线程执行。
另外在Java1.5之后,还有Callable、Future和FutureTask,在这里就不详细介绍,还有线程的wait、
yield、sleep等在下一章会一起详细介绍。
在Java中,线程的优先级有1~10,而默认的是5。1最低,10最高。在Thread类中有三个常量:
在同一个线程池中的线程优先级是相同的。
JVM会根据线程的优先级去抢先调度,然而线程的优先级只能保证抢占资源的概率较大,并不能保障线程的执行顺序,所以不能过于依赖设置线程的优先级。
频繁地创建和销毁线程会导致性能大幅度降低,这肯定不是你希望的。
线程池的出现,就是为了解决这个问题,根据java中提供不同的线程池机制,有效地提高资源利用率。
直接在代码中创建Thread、Runnable去start或者run容易出现不可预测的问题,在java1.5开始,引入了java.util.concurrent包,其中有个并发的框架:Executor,使用ExecutorService替代直接操作线程类,而Executors是用来创建线程池的,内部提供了很多静态方法去创建你想要的线程池,不需要你再手动去创建实现。
看一下关于Executor中的类和接口的大致的成员与关系:
关于这些类如何使用,以及有什么特性,下一章会作介绍。
在java中提到队列肯定会想起Queue,而线程队列用的是BlockingQueue,这是个接口,在concurrent包中有好几个类实现了这个接口。
介绍一下BlockingQueue常用的方法
在前面讲过死锁,死锁是由于使用不当引起的一种现象,而这里的锁是人工干预的,让并发按照你的意思走。
在java中的锁有synchonrized、Lock。锁的出现主要是为了解决线程安全问题。
关于线程的状态会在下一章讲锁的机制时候再讲,因为线程的状态会影响到锁。
线程安全的集合
因为多线程访问资源可能会造成数据不一致或者数据污染,而某些集合会用一些锁或者同步机制做了处理。
线程安全的集合有:HashTable、SynchronizedCollection、ConcurrentHashMap、Vector等。
线程安不安全的首要前提是在多线程访问同一个对象的情况下。
]]>《Java并发编程实践》
《Thinking in Java》
baoyongzhang(鲍老师)
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
1.View的坐标参数 主要有哪些?分别有什么注意的要点?
答:Left,Right,top,Bottom 注意这4个值其实就是 view 和 他的父控件的 相对坐标值。 并非是距离屏幕左上角的绝对值,这点要注意。
此外,X和Y 其实也是相对于父控件的坐标值。 TranslationX,TranslationY 这2个值 默认都为0,是相对于父控件的左上角的偏移量。
换算关系:
x=left+tranX,y=top+tranY.
很多人不理解,为什么事这样,其实就是View 如果有移动的话,比如平移这种,你们就要注意了,top和left 这种值 是不会变化的。
无论你把view怎么拖动,但是 x,y,tranX,tranY 的值是随着拖动平移 而变化的。想明白这点 就行了。
2.onTouchEvent和GestureDetector 在什么时候用哪个比较好?
答:只有滑动需求的时候 就用前者,如果有双击等这种行为的时候 就用后者。
3.Scroller 用来解决什么问题?
答:view的scrollTo和scrollBy 滑动效果太差了,是瞬间完成。而scroller可以配合view的computeScroll 来完成 渐变的滑动效果。体验更好。
4.ScrollTo和ScrollBy 有什么需要注意的?
答:前者是绝对滑动,后者是相对滑动。滑动的是view的内容 而不是view本身。这很重要。比如textview 调用这2个方法 滑动的就是显示出来的字的内容。
一般而言 我们用scrollBy会比较多一些。传值的话 其实 记住几个法则就可以了。 右-左 x为正 否则x为负 上-下 y为负,否则y为正。
可以稍微看一下 这2个的源码:
|
|
看到里面有2个变量 mScrollX 和mScrollY 这2个东西没,这2个单位的 值是像素,前者代表 view的左边缘和view内容左边缘的距离。 后者代表 view上边缘和view内容上边缘的距离。
5.使用动画来实现view的滑动 有什么后果?
答:实际上view动画 是对view的表面ui 也就是给用户呈现出的视觉效果 来做的移动,动画本身并不能移动view的真正位置。属性动画除外。动画播放结束以后,view最终还是会回到自己的位置的,。当然了你可以设置fillafter 属性 来让动画播放结束以后 view表象停留在 变化以后的位置。所以这会带来一个很严重的后果。比如你的button在屏幕的左边,你现在用个动画 并且设置了fillafter属性让他去了右边。你会发现 点击右边的button 没有click事件触发,但是点击左边的 却可以触发,原因就是右边的button 只是view的表象,真正的button 还在左边没有动过。你一定要这么做的话 可以提前在右边button移动后的位置放一个新的button,当你动画执行结束以后 把右边的enable 左边的让他gone就可以了。
这么做就可以规避上述问题。
6.让view滑动总共有几种方式,分别要注意什么?都适用于那些场景?
答:总共有三种:
a:scrollto,scrollby。这种是最简单的,但是只能滑动view的内容 不可以滑动view本身。
b:动画。动画可以滑动view内容,但是注意非属性动画 就如我们问题5说的内容 会影响到交互,使用的时候要多注意。不过多数复杂的滑动效果都是属性动画来完成的,属于大杀器级别、
c:改变布局参数。这种最好理解了,无非是动态的通过java代码来修改 margin等view的参数罢了。不过用的比较少。我本人不怎么用这种方法。
7.Scroller是干嘛的?原理是什么?
答:Scroller就是用于 让view有滑动渐变效果的。用法如下:
|
|
其实上述代码 很多人应该都能搜到。我们这里主要讲一下 他的原理。
|
|
8.view的滑动渐变效果总共有几种方法?
答:三种,第一种是scroller 也是使用最多的。问题7里有解释。还有一种就是动画,动画我就不多说了,不属于本文范畴。最后一种也是我们经常使用的就是用handler ,每隔一个时间间隔 来更新view的状态。
代码不写了很简单。 自行体会。
9.view的事件传递机制 如何用伪代码来表示?
答:
|
|
10.view的onTouchEvent,OnClickListerner和OnTouchListener的onTouch方法 三者优先级如何?
答:onTouchListener优先级最高,如果onTouch方法返回 false ,那onTouchEvent就被调用了,返回true 就不会被调用。至于onClick 优先级最低。
11.点击事件的传递顺序如何?
答:Activity-Window-View。从上到下依次传递,当然了如果你最低的那个view onTouchEvent返回false 那就说明他不想处理 那就再往上抛,都不处理的话
最终就还是让Activity自己处理了。举个例子,pm下发一个任务给leader,leader自己不做 给架构师a,小a也不做 给程序员b,b如果做了那就结束了这个任务。
b如果发现自己搞不定,那就找a做,a要是也搞不定 就会不断向上发起请求,最终可能还是pm做。
|
|
12.事件分为几个步骤?
答:down事件开头,up事件结尾,中间可能会有数目不定的move事件。
13.ViewGroup如何对点击事件分发?
答:
|
|
14.如果某个view 处理事件的时候 没有消耗down事件 会有什么结果?
答:假如一个view,在down事件来的时候 他的onTouchEvent返回false, 那么这个down事件 所属的事件序列 就是他后续的move 和up 都不会给他处理了,全部都给他的父view处理。
15.如果view 不消耗move或者up事件 会有什么结果?
答:那这个事件所属的事件序列就消失了,父view也不会处理的,最终都给activity 去处理了。
16.ViewGroup 默认拦截事件吗?
答:默认不拦截任何事件,onInterceptTouchEvent返回的是false。
17.一旦有事件传递给view,view的onTouchEvent一定会被调用吗?
答:是的,因为view 本身没有onInterceptTouchEvent方法,所以只要事件来到view这里 就一定会走onTouchEvent方法。
并且默认都是消耗掉,返回true的。除非这个view是不可点击的,所谓不可点击就是clickable和longgclikable同时为fale
Button的clickable就是true 但是textview是false。
18.enable是否影响view的onTouchEvent返回值?
答:不影响,只要clickable和longClickable有一个为真,那么onTouchEvent就返回true。
19.requestDisallowInterceptTouchEvent 可以在子元素中干扰父元素的事件分发吗?如果可以,是全部都可以干扰吗?
答:肯定可以,但是down事件干扰不了。
20.dispatchTouchEvent每次都会被调用吗?
答:是的,onInterceptTouchEvent则不会。
21.滑动冲突问题如何解决 思路是什么?
答。要解决滑动冲突 其实最主要的就是有一个核心思想。你到底想在一个事件序列中,让哪个view 来响应你的滑动?比如 从上到下滑,是哪个view来处理这个事件,从左到右呢?
用业务需求 来想明白以后 剩下的 其实就很好做了。核心的方法 就是2个 外部拦截也就是父亲拦截,另外就是内部拦截,也就是子view拦截法。 学会这2种 基本上所有的滑动冲突
都是这2种的变种,而且核心代码思想都一样。
外部拦截法:思路就是重写父容器的onInterceptTouchEvent即可。子元素一般不需要管。可以很容易理解,因为这和android自身的事件处理机制 逻辑是一模一样的
|
|
内部拦截法:内部拦截法稍微复杂一点,就是事件到来的时候,父容器不管,让子元素自己来决定是否处理。如果消耗了 就最好,没消耗 自然就转给父容器处理了。
子元素代码:
|
|
父亲容器代码也要修改一下,其实就是保证父亲别拦截down:
|
|
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>1. git init
初始化一个 Git 仓库(repository),即把当前所在目录变成 Git 可以管理的仓库。
2. git add 文件
把文件添加到 暂存区(stage),可被 track 追踪纪录下来。可多次使用来添加多个文件。
3. git add *
添加所有修改到暂存区,效果同 git add all,待验证。
4. git add -A
暂存所有的文件,包括新增加的、修改的和删除的文件。
5. git add .
暂存新增加的和修改的文件,不包括已删除的文件。即当前目录下所有文件。
6. git add -u
暂存修改的和删除的文件,不包括新增加的文件。
7. git add -i
交互式添加文件到暂存区。
8. git add -p
暂存文件的一部分。
9. git commit -m "本次提交说明"
一次性把暂存区所有文件修改提交到仓库的当前分支。注意:提交信息可为中文也可为英文,若为英文则通常用一般现在时。如果不加参数 -m 则会跳转到编辑器强制填写提交说明信息。
10. git commit -am "本次提交说明"
使用该命令,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤,参数 -am 也可写成 -a -m。“在 oh-my-zsh 下,直接用 gcam “message” 就搞定了”,—魔都三帅语。
11. git commit --amend
重新提交,最终只会有一个提交,第二次提交将代替第一次提交的结果。尤其适用于提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了的情况。
12. git commit --amend --reset-author
在上一次 commit 之后想重新更新一下时间。amend 实际上修改了上一个 commit。所以如果已经 push 了上一个 commit,请尽量不要 amend。如果一定要 amend 已经 push 了的 commit,请确保这个 commit 所在的 branch 只有你一个人使用(否则会给其他人带来灾难),然后在 amend 之后使用 git push –force。只要多加小心,该命令貌似没什么卵用。
13. git commit -p
commit 文件的一部分,适合工作量比较大的情况。之后,Git 会对每块修改弹出一个提示,询问你是否 stage,按 y/n 来选择是否 commit 这块修改,? 可以查看其他操作的说明。
1. git status
显示当前仓库的最新状态。提交之后,工作区就是“干净的”,即没有新的修改;有未提交文件时,最上面显示的是在 staging area,即将被 commit 的文件;中间显示没有 stage 的修改了的文件,最下面是新的还没有被 Git track 的文件。“在 oh-my-zsh 下,输入 gst 就出来了,谁用谁知道,装逼利器,效率杠杠的”,—魔都三帅语。
2. git status -s 或 git status --short
状态简览。输入此命令后,有如下几种情况(总共5种情况):新添加的未跟踪文件前面有 ?? 标记,新添加到暂你可能注意到了 M 有两个可以出现的位置,出现在右边的 M 表示该文件被修改了但是还没放入暂存区,出现在靠左边的 M 表示该文件被修改了并放入了暂存区。
3. git diff
查看工作区中的修改。
4. git diff --staged 或 git diff --cached
查看暂存区中的修改。
5. git diff <commit id1> <commit id2>
比较两次 commit 之间的差异。
6. git diff <branch1> <branch2>
在两个 branch 之间比较。
7. git diff 文件
查看指定文件具体修改了哪些内容。
8. git diff HEAD -- 文件
查看版本库最新版本和工作区之间的区别,貌似没什么卵用。
9. git difftool --tool-help
查看系统支持哪些 Git Diff 插件,貌似没什么卵用。
10. git show
查看最后一个 commit 的修改。
11. git show HEAD~3
查看倒数第四个 commit 的修改,HEAD~3 就是向前数三个的 commit,即倒数第四个 commit。
12. git show deadbeef
查看 hash 为 deadbeef 的 commit 的修改。
13. git blame 文件
查看谁什么时间改了哪些文件。
1. git log
显示从最近到最远的提交日志,包括每个提交的 SHA-1 校验和、作者的名字和电子邮件地址、提交时间以及提交说明等基本信息。
2. git log -p -2
除显示基本信息之外,还显示每次提交的内容差异,-2 意思是仅显示最近两次提交。特别适用于进行代码审查,或者快速浏览某个搭档提交的 commit 所带来的变化。
3. git log --start
显示每次提交的简略的统计信息,貌似不太好用。
4. git log --graph
查看分支合并图。
5. git log --pretty=oneline
简化日志信息,将每个提交放在一行显示,查看的提交数很大时非常有用,也可带有 –graph 参数,效果同 git config format.pretty oneline。
6. git log --graph --pretty=oneline --abbrev-commit
查看分支的合并情况,包括分支合并图、一行显示、提交校验码缩略显示。
7. git log --oneline --decorate
查看各个分支当前所指的提交对象(commit object)。Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个树对象(记录着目录结构和 blob 对象索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息)。
8. git log --oneline --decorate --graph --all
查看分叉历史,包括:提交历史、各个分支的指向以及项目的分支分叉情况。
9. git reset --hard HEAD^
回退到上一个版本。同理,回退到上上个版本为:HEAD^ ^, 回退到上100个版本为:HEAD-100,貌似波浪号 ~ 也可以,变成倒数第101个。
10. git reflog
纪录每一次命令,可用于查找某一提交版本的 commit id。
11. git reset --hard <commit id>
回退到某一提交过的版本,如果已经 push,则回退的意义不大了。恢复一个彻底删掉的 commit,见链接:https://github.com/xhacker/GitProTips/blob/master/zh_CN.md#别人-push-了修改我无法-push-了怎么办。
1. 工作区(Working Directory)
项目所在的文件目录。
2. 版本库(Repository)
工作区有一个隐藏目录文件 .git(可通过命令 ls -ah 查看隐藏文件),这就是 Git 的版本库。版本库里主要有称为 stage 的暂存区、Git 自动创建的 master 分支,以及指向 master 的一个指针 HEAD,表示版本库的最新版本。
1. git checkout -- 文件
丢弃工作区的修改,包括修改后还没有放到暂存区和添加到暂存区后又作了修改两种情况。总之,让该文件回到最近一次 git commit 或 git add 之后的状态。注意:没有 – ,就变成了切换分支的命令了。郭神的书 P195 没有 – 符号,可能是笔误。
2. git reset HEAD 文件
把暂存区的修改撤销(unstage),回退到工作区。注意:在 Git 中任何已提交的东西几乎总是可以恢复的。甚至那些被删除的分支中的提交或使用 –amend 选项覆盖的提交也可以恢复。然而,任何未提交的东西丢失后很可能再也找不到了。
3. git reset --hard
重置所有文件到未修改的状态。
4. git reset <commit SHA>
重置到某个 commit。
5. git reset HEAD~1
将当前 branch 重置为倒数第二个 commit(即丢弃最后一个 commit)。git reset 有三个参数可以选择,–soft、–mixed 和 –hard。
6. git reset --soft
修改最后一个 commit。貌似没什么卵用。
7. git revert <commit id>
还原某个 commit。还原(revert)的实质是产生一个新的 commit,内容和要还原的 commit 完全相反。比如,A commit 在 main.c 中增加了三行,revert A 产生的 commit 就会删除这三行。如果我们非常确定之前的某个 commit 产生了 bug,最好的办法就是 revert 它。git revert 后 git 会提示写一些 commit message,此处最好简单描述为什么要还原;而重置(reset)会修改历史,常用于还没有 push 的本地 commits。
8. git revert HEAD
还原到上次 commit。
1. git rm 文件
把文件从版本库中删除,不会再追踪到。
2. git rm -f 文件
强制删除版本库中有修改的文件。
3. git rm --cached 文件
把文件从版本库中删除,但让文件保留在工作区且不被 Git 继续追踪(track),通常适用于在 rm 之后把文件添加到 .gitignore 中的情况。
4. git rm log/\*.log
删除 log/ 目录下扩展名为 .log 的所有文件。
5. git rm \*~
删除以 ~ 结尾的所有文件。
1. git remote
查看已经配置的远程仓库服务器,效果同 git remote show。
2. git remote -v
显示需要读写远程仓库使用的 Git 保存的简写与其对应的 URL。
3. git ls-remote <remote-name>
显示获得远程引用的完整列表。
4. git remote show <remote-name>
参数 remote-name 通常都是缩写名 origin,可以得到远程分支更为详细的信息以及 pull 和 push 相关提示信息。
5. git remote add <shortname> <url>
添加并关联一个远程库。其中,shortname 一般是 origin,也可以是其他字符串,用来代替整个 url。
6. git push
推送本地修改到 origin。
7. git push -u origin master
关联后,使用该命令第一次推送 master 分支的所有内容,后续再推送的时候就可以省略后面三个参数了,其中参数 u 代表上游(upstream)的意思。
8. git push origin 远程分支(通常是 master)
推送最新修改。注意:多人协作时,除了 merge 可能会发生冲突之外,推送时也有可能发生冲突。在他人推送之后是不能立即推送自己的修改的,想想也是,因为可能会覆盖他人的工作,所以必须先拉取(pull)别人的修改合并(merge)之后才能推送。如果不是第一次推送,后面的参数可省略。
9. git push <remote-name> <commit SHA>:<remote-branch_name>
push 一部分 commit。例如:git push origin 9790eff:master 即为 push 9790eff 之前的所有 commit 到 master。
10. git remote rename old_name new_name
重命名一个远程仓库的简写名。
11. git remote rm <remote-name>
移除一个远程仓库。
12. git remote add origin http://github.com/username/<repo name>.git
Create a remote repo named origin pointing at your Github repo (after you’ve already created the repo on Github) (used if you git init since the repo you created locally isn’t linked to a remote repo yet).
13. git remote add origin git@github.com:username/<repo name>.git
Create a remote repo named origin pointing at your Github repo (using SSH url instead of HTTP url).
1. git clone git@github.com:username/<repo name>.git
从远程库(origin)克隆一份到本地,仓库名同远程仓库名。
2. git clone https://github.com/username/repo name.git
作用同上。但不建议使用 https 协议,原因有二:一是速度慢;二是每次推送必须输入口令,麻烦。但在某些只开放 http 端口的公司内部就无法使用原生的 ssh 协议而只能用 https,仓库名同远程仓库名。
3. git clone <repo url> <folder name>
克隆一个仓库到指定文件夹。
4. git clone <repo url> .
克隆一个仓库到当前文件夹(应该是空的)。
1. git branch
列出本地当前所有分支,方便查看。当前分支前面会标有一个 * 号。
2. git branch -r
查看远程分支列表。
3. git branch -a
显示所有分支,包括本地和远程。
4. git branch -v
查看每一次分支的最后一次提交。
5. git branch -vv
查看设置的所有跟踪分支。将所有的本地分支列出来并且包含更多的信息,如每一个分支正在跟踪哪个远程分支与本地分支是否是领先、落后或是都有。
6. git branch --merged
查看所有已经被 merge 的 branch。
7. git branch --no-merged
查看所有还没被 merge 的 branch。
8. git branch --merged | xargs git branch -d
删除所有已经被 merge 的 branch。
9. git checkout -b 分支
创建并切换到新的分支,相当于下面两条命令:git branch 分支 + git checkout 分支。
10. git checkout -
切换到上一个 branch。
11. git cherry-pick <commit id>
假如我们在某个 branch 做了一大堆 commit,而当前 branch 想应用其中的一个,可以使用该命令。
12. git merge 分支
合并指定分支到当前所在的分支。
13. git merge --no-ff -m "提交说明信息" 分支
参数 –no-ff 表示禁用 Fast forward 快进模式,用普通模式合并,这样合并后的历史有分支,能看出来曾经做过合并,而 fast forwad 合并就看不出来曾经做过合并。
14. git branch -d 分支
普通删除分支(相对强制删除而言)。一般情况下,先合并完分支,然后再删除,否则会删除失败,除非使用 -D 参数强制删除。注意:因为创建、合并和删除分支非常快,所以 Git 鼓励使用分支完成某个任务,合并后再删除分支,这个直接在 master 分支上工作效果是一样的,但过程更安全。
15. git branch -D 分支
强行删除分支,尤其适用分支内容有了新的修改但还没有被合并的情况。
16. git push origin --delete 远程分支 或 git push origin:远程分支
删除一个远程分支。基本上这个命令做的只是从服务器上移除这个指针。 Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。
17. git push origin -delete 分支
在本地和远程同步删除分支。
18. git rebase 目标分支(通常是 master)
在本地 master 上进行变基操作。注意:merge 与 rebase 都是整合来自不同分支的修改。
merge 会把两个分支的最新快照以及二者最近的共同祖先进行三方合并,合并的结果是生成一个新的快照(并提交)。
rebase 会把提交到某一分支(当前分支)上的所有修改都转移至另一分支(目标分支)上,就好像“重新播放”一样。
变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。简言之:这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。
采用变基操作后,项目的最终维护者就不再需要进行整合工作,只需要快进合并便可。
git rebase –ongo
目标分支 第一分支 第二分支:选中在第二分支里但不在第一分支里的修改,将它们在目标分支(通常是 master)上重演。
变基有风险,需要遵守的准则是:不要对在你的仓库外有副本的分支执行变基。否则,会导致混乱。总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作,这样才能享受到两种方式带来的便利。
还可以有这样的命令:
git rebase -i master,git rebase -i 22e21f2,git rebase -i HEAD~3。
STASH
git stash
把当前分支的工作现场储存起来,等以后恢复现场后继续工作。一般适用于还没有 commit 的分支代码。
git stash list
查看储存的工作现场纪录列表。
git stash apply + git stash drop
用 git stash apply 命令恢复最近 stash 过的工作现场,但是恢复后,stash 内容并不删除,用 git stash drop 命令来删除。apply 和 drop 后面都可以加上某一指定的 stash_id。
git stash pop
相当于上面两条命令,恢复回到工作现场的同时把 stash 内容也删除了。
git stash clear
清空所有暂存区的 stash 纪录。drop 是只删除一条,当然后面可以跟 stash_id 参数来删除指定的某条纪录,不跟参数就是删除最近的。
git stash apply stash@{0}
上面命令中大括号中的数字不是固定的,因为可以多次 stash,恢复的时候,先用 git stash list 命令查看,然后恢复指定的 stash。
git biselect
发现了一个 bug,用该命令知道是哪个 commit 导致的,貌似不太好用。
PULL AND PUSH
git push origin 分支
把该分支上的所有本地提交推送到远程库对应的远程分支上。
git checkout 分支 origin/分支
如果远程有某一个分支而本地没有,怎用该命令把远程的这个分支迁到本地。
git checkout -b 分支 origin/分支
把远程分支迁到本地顺便切换到该分支。
git pull
抓取远程库最新提交,拉取并合并。
git fetch
没有 merge 的 pull。
git branch –set-upstream 分支 origin/分支
建立本地分支和远程分支的关联。
git submodule update –recursive
第三方依赖与远程同步,还可以在最后添加 -f 参数。
1. git tag
查看所有标签。注意:标签不是按照时间列出,而是按照字母排序,但这并不重要。
2. git show <tag-name>
查看标签信息。
3. git tag -l 'tag-name'
使用特定的模式查找标签。
4. git checkout <tag-name>
切换 tag。
5. git tag <tag name> <commit id>
在需要打标签的分支上创建一个轻量标签(lightweight),默认为 HEAD,也可以指定一个 commit id。
6. git tag -a <tag-name> -m "标签说明文字" <commit id>
创建附注标签(annotated),用 -a 指定标签名,-m 指定说明文字,也可以指定一个 commit id。
7. git tag -a <tag-name> 提交的校验和或部分校验和
后期打标签,即对过去的提交打标签。校验和(checksum):长度为 40位的16进制数的 SHA-1 值字符串。然而,只要没有冲突,通常可以用一个比较短的前缀来表示一个 commit。
8. git tag -s <tag-name> -m "标签说明文字" <commit id>
通过 -s 用私钥签名一个标签。签名采用 GPG 签名,因此,必须首先按照 pgp(GnuPG),如果没有找到 gpg,或者没有 gpg 秘钥对,就会报错。如果报错,请参考 GnuPG 帮助文档配置 Key。
1. git tag -d <tag-name>
删除一个本地标签。因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。
2. git push origin <tag-name>
推送本地某个标签到远程,默认情况下,git push 命令并不会推送标签到远程,必须显示推送。
3. git push origin --tags
参数 –tags 表示一次性推送全部未推送到远程的本地标签,当其他人从仓库中克隆或拉取,他们也能得到那些标签。
4. git push origin :refs/tags/<tag-name>
删除一个远程标签,先从本地删除,再用该命令从远程删除。
5. git checkout -b <branch-name> <tag-name>
在特定的标签上创建一个新分支,貌似没什么卵用。
1. git add -f 文件
使用 -f 参数,强制添加被 .gitignore 忽略的文件到 Git。
2. git check-ignore -v 文件
可能是 .gitignore 写得有问题,使用该命令找出到底哪个命令写错了。
3. https://github.com/github/gitignore
GitHub 上的一个十分详细的针对数十种项目及语言的 .gitignore 文件列表。
注意:
忽略某些文件时,需要编写 .gitignore 文件;
.gitignore 文件本身要放到版本库里,并且可以对 .gitignore 做版本管理。
ALIAS
1. git config --global alias.st status
使用 git st 代替 git status 命令。
2. git config --global alias.co checkout
使用 git co 代替 git checkout 命令。
3. git config --global alias.cm commit
使用 git cm 代替 git commit 命令。
4. git config --global alias.br branch
使用 git br 代替 git branch 命令。
5. git config --global alias.unstage 'reset HEAD --'
使用 git unstage 文件 命令代替 git reset HEAD – 文件 命令。
6. git config --global alias.last 'log -1'
配置一个 git last 命令,让其显示最近一次的提交信息。
`7. git config --global alias.lg "log --color --graph --`pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
丧心病狂地配置 git lg 命令,让显示 log 更加优雅,逼格更高。
1. git config -l 或 git config --list
列举所有 Git 能找到的配置,如果有重复的变量名,Git 会使用它找到的每一个变量的最后一个配置。
2. git config <key>
检查 Git 的某一项配置。
3. git config --glabal core.editor <vim/emacs/...>
配置默认文本编辑器。
4. git config --global color.ui true
让 Git 显示颜色,使命令输出看起来更醒目。
5. git config core.ignorecase false
Git 是大小写不敏感的,如果要大小写敏感需要执行此命令。
6. git config --global core.quotepath false
设置显示中文文件名。
7. it config --global credential.helper cache
如果正在使用 HTTPS URL 来推送,Git 服务器会询问用户名与密码。 默认情况下它会在终端中提示服务器是否允许你进行推送。如果不想在每一次推送时都输入用户名与密码,可以设置一个 “credential cache”。 最简单的方式就是将其保存在内存中几分钟,使用该命令即可,貌似没什么卵用。
8. git config --global user.name "your name"
git config --global user.email "your email"
设置 commit 中的姓名和 email,去掉 –global 参数则为针对每个 repo 单独设定姓名和邮箱。
9. git commit --author "your name <your email>"
以其他身份 commit。
10. git mv old_filename new_filename
重命名文件。相当于下面三条命令:
11. git log 常用选项
-p — 按补丁格式显示每个更新之间的差异。
-stat — 显示每次更新的文件修改统计信息。
-shortstat — 只显示 –stat 中最后的行数修改添加移除统计。
-name-only — 仅在提交信息后显示已修改的文件清单。
-name-status – 显示新增、修改、删除的文件清单。
-abbrev-commit — 仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。
-relative-date — 使用较短的相对时间显示(比如,“2 weeks ago”)。
-graph — 显示 ASCII 图形表示的分支合并历史。
-pretty — 使用其他格式显示历史提交信息。可用的选项包括 oneline,short,full,fuller 和 format(后跟指定格式)。
12. git log --pretty=format:"%h - %an, %ar : %s"
自定义 log 显示样式,也可带有 –graph 参数。常用的格式占位符写法及其代表的意义:
%H — 提交对象(commit)的完整哈希字串
%h — 提交对象的简短哈希字串
%T — 树对象(tree)的完整哈希字串
%t — 树对象的简短哈希字串
%P — 父对象(parent)的完整哈希字串
%p — 父对象的简短哈希字串
%an — 作者(author)的名字
%ae — 作者的电子邮件地址
%ad — 作者修订日期(可以用 –date= 选项定制格式)
%ar — 作者修订日期,按多久以前的方式显示
%cn — 提交者(committer)的名字
%ce — 提交者的电子邮件地址
%cd — 提交日期
%cr — 提交日期,按多久以前的方式显示
%s — 提交说明
13. git log --since=2.weeks
显示按照时间限制的 log 信息,这个时间格式可以是:“2008-01-15” 或 “2 years 1 day 3 minutes ago” 等。可用的参数还有:–until,–author,–grep(提交说明中的关键字)等。注意:如果要得到同时满足这两个选项搜索条件的提交,就必须用 –all-match 选项。否则,满足任意一个条件的提交都会被匹配出来。
14. git log -Sfunction_name
显示添加或移除某一个特定函数的引用(字符串)的提交。
15. 限制 git log 输出的选项
-(n) — 仅显示最近的 n 条提交
–since, –after — 仅显示指定时间之后的提交。
–until, –before — 仅显示指定时间之前的提交。
–author — 仅显示指定作者相关的提交。
–committer — 仅显示指定提交者相关的提交。
–grep — 仅显示含指定关键字的提交
-S — 仅显示添加或移除了某个关键字的提交
For example,git log –pretty=”%h - %s” –author=gitster –since=”2008-10-01” \ –before=”2008-11-01” –no-merges – t/,即为:查看 Git 仓库中,2008 年 10 月期间,作者提交的但未合并的测试文件。
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>1.头文件中存放的是对某个库中所定义的函数、宏(define)、类型、全局变量等进行声明,它类似于一份仓库清单。若用户程序中需要使用某个库中的函数,则只需要将该库所对应的头文件include到程序中即可。
2.头文件中定义的是库中所有函数的函数原型。而函数的具体实现则是在库文件中。
3.在连接器连接程序时,会依据用户程序中导入的头文件,将对应的库函数导入到程序中。头文件以.h为后缀名。
头文件是给编译器用的,库文件是给连接器用的
### 函数库:
1.动态库:在编译用户程序时不会将用户程序内使用的库函数连接到用户程序的目标代码中,只有在运行时,且用户程序执行到相关函数时才会调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。
2.静态库:在编译用户程序时会将其内使用的库函数连接到目标代码中,程序运行时不再需要静态库。使用静态库生成可执行文件比较大。
为什么要进行交互?
首先,java语言提供的类库无法满足要求,且在数学运算,实时渲染的游戏上,音视频处理等方面上与c/c++相比效率稍低。然后,java语言无法直接操作硬件,c/c++代码不仅能操作硬件而且还能发挥硬件最佳性能。接着,使用java调用本地的c/c++代码所写的库,省去了重复开发的麻烦,并且可以利用很多开源的库提高程序效率。
Java调用C/C++大概有这样几个步骤
我们对这个还是很清楚的,看代码:
c代码:
|
|
c++代码:
|
|
从上面Native函数的命名上我们可以了解到JNI函数的命名规则: Java代码中的函数声明需要添加native 关键 字;Native的对应函数名要以“Java”开头,后面依次跟上Java的“package名”、“class名”、“函数名”,中间以下划线“” 分割,在package名中的“.”也要改为“_”。此外,关于函数的参数和返回值也有相应的规则。对于Java中的基本类型如int 、double 、char 等,在Native端都有相对应的类型来表示,如jint 、jdouble 、jchar 等;其他的对象类型则统统由jobject 来表示(String 是个例外,由于其使用广泛,故在Native代码中有jstring 这个类型来表示,正如在上例中返回值String 对应到Native代码中的返回值jstring )。而对于Java中的数组,在Native中由jarray 对应,具体到基本类型和一般对象类型的数组则有jintArray 等和jobjectArray 分别对应(String 数组在这里没有例外,同样用jobjectArray 表示)。还有一点需要注意的是,在JNI的Native函数中,其前两个参数JNIEnv 和jobject 是必需的——前者是一个JNIEnv 结构体的指针,这个结构体中定义了很多JNI的接口函数指针,使开发者可以使用JNI所定义的接口功能;后者指代的是调用这个JNI函数的Java对象,有点类似于C++中的this 指针。在上述两个参数之后,还需要根据Java端的函数声明依次对应添加参数。在上例中,Java中声明的JNI函数没有参数,则Native的对应函数只有类型为JNIEnv 和jobject 的两个参数。
效果图:
一般来说,要在Native代码中访问Java对象,有如下几个步骤:
得到该Java对象的类定义。JNI定义了jclass 这个类型来表示Java的类的定义,并提供了FindClass接口,根据类的完整的包路径即可得到其jclass 。
根据jclass 创建相应的对象实体,即jobject 。在Java中,创建一个新对象只需要使用new 关键字即可,但在Native代码中创建一个对象则需要两步:首先通过JNI接口GetMethodID得到该类的构造函数,然后利用NewObject接口构造出该类的一个实例对象。
访问jobject 中的成员变量或方法。访问对象的方法是先得到方法的Method ID,然后使用Call
寻找class对象, 并实例化
JVM在Java中都是自己启动的, 在C/C++中只能自己来启动了, 启动完之后的事情就和在Java中一样了, 不过要使用C/C++的语法.
获取class对象比较简单, FindClass(env, className).
cls = (*env)->FindClass(env, "xxxx");
在Java中的类名格式是java.lang.String, 但是className的格式有点不同, 不是使用’.’作为分割, 而是’/‘, 即java/lang/String.
我们知道Java中构造函数有两种, 一种是默认的没有参数的, 一种是自定义的带有参数的. 对应的在C/C++中, 有两种调用构造函数的方法.
调用默认构造函数
// 调用默认构造函数 obj = (*env)->AllocObjdect(env, cls);
构造函数也是方法, 类似调用方法的方式.
// 调用指定的构造函数, 构造函数的名字叫做<init> mid = (*env)->GetMethodID(env, cls, "<init>", "()V"); obj = (*env)->NewObject(env, cls, mid);
调用方法和修改属性
关于方法和属性是有两个ID与之对应, 这两个ID用来标识方法和属性.
jmethodID mid; jfieldID fid;
方法分为静态和非静态的, 所以对应的有
mid = (*env)->GetStaticMethodID(env, cls, "sayHello", "(Ljava/lang/String;)Ljava/lang/String;"); mid = (*env)->GetMethodID(env, cls, "sayHello", "()Ljava/lang/String;");
上面两个方法是同名的, 都叫sayHello, 但是签名不同, 所以可以区分两个方法.
JNI的函数都是有一定规律的, Static就表示是静态, 没有表示非静态.
方法的调用如下
jstring result = (jstring)(*env)->CallStaticObjectMethod(env, cls, mid, arg); jstring result = (jstring)(*env)->CallObjectMethod(env, obj, mid);
我们可以看到静态方法是只需要class对象, 不需要实例的, 而非静态方法需要使用我们之前实例化的对象.
属性也有静态和非静态, 示例中只有非静态的.
获取属性ID
fid = (*env)->GetFieldID(env, cls, "name", "Ljava/lang/String;");
改属性的值
(*env)->SetObjectField(env, obj, fid, arg); // 修改属性
关于jstring的说明
java的String都是使用了unicode, 是双字节的字符, 而C/C++中使用的单字节的字符。
从C转换为java的字符, 使用NewStringUTF方法
jstring arg = (*env)->NewStringUTF(env, name);
从java转换为C的字符, 使用GetStringUTFChars
const char* str = (*env)->GetStringUTFChars(env, result, 0);
下面我们来看代码:
c代码:
|
|
c++代码:
|
|
可以看到,上述代码和前面讲到的步骤完全相符。这里提一下编程时要注意的要点:1、FindClass要写明Java类的完整包路径,并将 “.”以“/”替换;2、GetMethodID的第三个参数是方法名(对于构造函数一律用“
关于上面谈到的步骤再补充说明一下:在JNI规范中,如上这种使用NewObject创建的对象实例被称为“Local Reference”,它仅在创建它的Native代码作用域内有效,因此应避免在作用域外使用该实例及任何指向它的指针。如果希望创建的对象实例在作用 域外也能使用,则需要使用NewGlobalRef接口将其提升为“Global Reference”——需要注意的是,当Global Reference不再使用后,需要显式的释放,以便通知JVM进行垃圾收集。
顺便看下截图:
在Android使用Jni时,为了能够使UI线程即主线程与工作线程分开,经常要创建工作线程,然后在工作线程中调用C/C++函数.为了在C/C++ 函数中更新Android的UI,又时常使用回调。jni更新ui的话,我们就要注重jobject的使用了。
看代码:(使用)
|
|
c代码 :
|
|
c++代码:
|
|
效果图:
Java调用C和C++函数时的JNI使用区别:
注意:jni.h头文件中对于.c & .cpp采用不同的定义
在C的定义中,env是一个两级指针,而在C++的定义中,env是个一级指针
C形式需要对env指针进行双重deferencing,而且须将env作为第一个参数传给jni函数
|
|
对于*.c
1.jclass test_class = (*env)->GetObjectClass(env, obj);
2.jfieldID id_num = (*env)->GetFieldID(env, test_class, “num”, “I”);
对于 *.cpp
1.jclass test_class = env->GetObjectClass(obj);
2.jfieldID id_num = env->GetFieldID(test_class, “num”, “I”);
在 C 中,
JNI 函数调用由“(*env)->”作前缀,目的是为了取出函数指针所引用的值。
在 C++ 中,
JNIEnv 类拥有处理函数指针查找的内联成员函数。
下面将说明这个细微的差异,其中,这两行代码访问同一函数,但每种语言都有各自的语法。
C 语法:jsize len = (*env)->GetArrayLength(env,array);
C++ 语法:jsize len =env->GetArrayLength(array);
1、jni 可以调用本地C函数。
2、jni 调用C++库时,首先要将C++库提供的功能封装成纯C格式的函数接口,然后jni里面调用这些C接口。
总结,没什么区别。一个是 jni调用c。另一个是jni调用c,c调用c++。
传送门:jnimaster
##总结
JNI使用c和cpp的基本使用和了解就讲的差不多了,更多的学习可以去看jni的使用安全手册。
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>对明确指出了目标组件名称的Intent,我们称之为“显式Intent”。
对于没有明确指出目标组件名称的Intent,则称之为“隐式 Intent”。
对于隐式意图,在定义Activity时,指定一个intent-filter,当一个隐式意图对象被一个意图过滤器进行匹配时,将有三个方面会被参考到:
动作(Action)
类别(Category [‘kætɪg(ə)rɪ] )
数据(Data )
|
|
1.线程池
答:线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。就好比原来去食堂打饭是每个人看谁抢的赢,谁先抢到谁先吃,有了线程池之后,就是排好队形,今天我跟你关系好,你先来吃饭。比如:一个应用要和网络打交道,有很多步骤需要访问网络,为了不阻塞主线程,每个步骤都创建个线程,在线程中和网络交互,用线程池就变的简单,线程池是对线程的一种封装,让线程用起来更加简便,只需要创一个线程池,把这些步骤像任务一样放进线程池,在程序销毁时只要调用线程池的销毁函数即可。
单个线程的弊端:a. 每次new Thread新建对象性能差b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或者OOM,c. 缺乏更多功能,如定时执行、定期执行、线程中断。
2、Java 线程池
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
(1). newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
(2). newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
(3) newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。ScheduledExecutorService比Timer更安全,功能更强大
(4)、newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
答:
这道面试题,主要是考察对多线程的理解和使用,还涉及到网络请求和文件操作,使用的技术多种多样,一般可以回答: 使用Handler(可以看No.8Handler的原理),AsyncTask的处理原理,没写过去百度一下,网上一堆教你怎么做的,不必死扣代码,只需要明白逻辑流程,能够完整流畅的说清楚为什么,这个题目也就回答到点子上了。
一、IntentService简介
IntentService是Service的子类,比普通的Service增加了额外的功能。先看Service本身存在两个问题:
- Service不会专门启动一条单独的进程,Service与它所在应用位于同一个进程中;
- Service也不是专门一条新线程,因此不应该在Service中直接处理耗时的任务;
二、IntentService特征
- 会创建独立的worker线程来处理所有的Intent请求;
- 会创建独立的worker线程来处理onHandleIntent()方法实现的代码,无需处理多线程问题;
- 所有请求处理完成后,IntentService会自动停止,无需调用stopSelf()方法停止Service;
- 为Service的onBind()提供默认实现,返回null;
- 为Service的onStartCommand提供默认实现,将请求Intent添加到队列中;
- IntentService不会阻塞UI线程,而普通Serveice会导致ANR异常
- Intentservice若未执行完成上一次的任务,将不会新开一个线程,是等待之前的任务完成后,再执行新的任务,等任务完成后再次调用stopSelf
答:通过ContentObserver的方式进行数据变化监听,要说清楚ContentObserver怎么用的,面试官有可能会问你观察者模式。最好是写一编观察者模式。理解后就好说明用法或者原理
答:
通过Binder对象
当Activity通过调用bindService(Intent service, ServiceConnection conn,int flags),我们可以得到一个Service的一个对象实例,然后我们就可以访问Service中的方法,从而根据数据不同来更新
通过BroadCast(广播)的形式
当我们的进度发生变化的时候我们发送一条广播,然后在Activity的注册广播接收器,接收到广播之后更新视图
答:
通过 onSaveInstanceState() 和 onRestoreInstanceState() 保存和重启非持久化数据。
答:
handler干了些什么:
运行在某个线程上,共享线程的消息队列;
接收消息、调度消息,派发消息和处理消息;
实现消息的异步处理;
建立消息处理模型/系统
参考博客
答:
Holo Theme
Holo Theme 是 Android Design 的最基础的呈现方式。因为是最为基础的 Android Design 呈现形式,每一台 Android 4.X 的手机系统内部都有集成 Holo Theme 需要的控件,即开发者不需要自己设计控件,而是直接从系统里调用相应的控件。在 UI 方面没有任何的亮点,和 Android4.X 的设置/电话的视觉效果极度统一。由此带来的好处显而易见,这个应用作为 Android 应用的辨识度极高,且完全不可能与系统风格产生冲突。
Material Design
Material design其实是单纯一种设计语言,它包含了系统界面风格、交互、UI,更加专注拟真,更加大胆丰富的用色,更加丰富的交互形式,更加灵活的布局形式
1.鲜明、形象的界面风格,
2.色彩搭配使得应用看起来非常的大胆、充满色彩感,凸显内容
3.Material design对于界面的排版非常的重视
4.Material design的交互设计上采用的是响应式交互,这样的交互设计能把一个应用从简单展现用户所请求的信息,提升至能与用户产生更强烈、更具体化交互的工具。
答:
三者在执行速度方面的比较:StringBuilder > StringBuffer > String
String每次变化一个值就会开辟一个新的内存空间
StringBuilder:线程非安全的
StringBuffer:线程安全的
对于三者使用的总结:
1.如果要操作少量的数据用 = String
2.单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
3.多线程操作字符串缓冲区 下操作大量数据 = StringBuffer
答:BigDecimal类进行商业计算,Float和Double只能用来做科学计算或者是工程计算
答:
Java的String和C++的string是不能对等起来的,所以当我们拿到.h文件下面的jstring对象,会做一次转换我们把jstring转换为C下面的char*类型,
constchar* str;
str = env->GetStringUTFChars(prompt,false); 获取值
char* tmpstr =”return string succeeded”;
jstring rtstr = env->NewStringUTF(tmpstr);赋予值
答:
Application的Context是一个全局静态变量,SDK的说明是只有当你引用这个context的生命周期超过了当前activity的生命周期,而和整个应用的生命周期挂钩时,才去使用这个application的context。
在android中context可以作很多操作,但是最主要的功能是加载和访问资源。在android中有两种context,一种是 application context,一种是activity context,通常我们在各种类和方法间传递的是activity context。
答:
1.onmesarue() 为整个View树计算实际的大小
2.onlayout() 为将整个根据子视图的大小以及布局参数将View树放到合适的位置上
3.ondraw()
1 、绘制该View的背景
2 、为显示渐变框做一些准备操作(见5,大多数情况下,不需要改渐变框)
3、调用onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)
4、调用dispatchDraw ()方法绘制子视图(如果该View类型不为ViewGroup,即不包含子视图,不需要重载该方法)
答:
1、基础知识
(1) 所有Touch事件都被封装成了MotionEvent对象,包括Touch的位置、时间、历史记录以及第几个手指(多指触摸)等。
(2) 事件类型分为ACTION_DOWN, ACTION_UP, ACTION_MOVE, ACTION_POINTER_DOWN, ACTION_POINTER_UP, ACTION_CANCEL,每个事件都是以ACTION_DOWN开始ACTION_UP结束。
(3) 对事件的处理包括三类,分别为
传递——dispatchTouchEvent()函数、
拦截——onInterceptTouchEvent()函数、
消费——onTouchEvent()函数和OnTouchListener
2、传递流程
(1) 事件从Activity.dispatchTouchEvent()开始传递,只要没有被停止或拦截,从最上层的View(ViewGroup)开始一直往下(子View)传递。子View可以通过onTouchEvent()对事件进行处理。
(2) 事件由父View(ViewGroup)传递给子View,ViewGroup可以通过onInterceptTouchEvent()对事件做拦截,停止其往下传递。
(3) 如果事件从上往下传递过程中一直没有被停止,且最底层子View没有消费事件,事件会反向往上传递,这时父View(ViewGroup)可以进行消费,如果还是没有被消费的话,最后会到Activity的onTouchEvent()函数。
(4) 如果View没有对ACTION_DOWN进行消费,之后的其他事件不会传递过来。
(5) OnTouchListener优先于onTouchEvent()对事件进行消费。
上面的消费即表示相应函数返回值为true。
(1) View不处理事件流程图
(2) View处理事件流程图
答:
1.逐帧动画
逐帧动画的工作原理很简单,其实就是将一个完整的动画拆分成一张张单独的图片,然后再将它们连贯起来进行播放,类似于动画片的工作原理
2.补间动画
补间动画则是可以对View进行一系列的动画操作,包括淡入淡出、缩放、平移、旋转四种
缺陷:补间动画是只能够作用在View上,它只能够实现移动、缩放、旋转和淡入淡出这四种动画操作,补间动画还有一个致命的缺陷,就是它只是改变了View的显示效果而已,而不会真正去改变View的属性
逐帧动画和补间动画它们的技术已经比较老了,而且网上资料也非常多
3.属性动画
ValueAnimator
ValueAnimator是整个属性动画机制当中最核心的一个类
ObjectAnimator
相比于ValueAnimator,ObjectAnimator可能才是我们最常接触到的类,因为ValueAnimator只不过是对值进行了一个平滑的动画过渡,但我们实际使用到这种功能的场景好像并不多。而ObjectAnimator则就不同了,它是可以直接对任意对象的任意属性进行动画操作的,比如说View的alpha属性
答:
因为Java程序编译之后的代码不是能被硬件系统直接运行的代码,而是一种“中间码”——字节码。然后不同的硬件平台上安装有不同的Java虚拟机(JVM),由JVM来把字节码再“翻译”成所对应的硬件平台能够执行的代码。因此对于Java编程者来说,不需要考虑硬件平台是什么。所以Java可以跨平台。
HTPPS和HTTP的概念
HTTPS(全称:Hypertext Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。 它是一个URI scheme(抽象标识符体系),句法类同http:体系。用于安全的HTTP数据传输。https:URL表明它使用了HTTP,但HTTPS存在不同于HTTP的默认端口及一个加密/身份验证层(在HTTP与TCP之间)。这个系统的最初研发由网景公司进行,提供了身份验证与加密通讯方法,现在它被广泛用于万维网上安全敏感的通讯,例如交易支付方面。
超文本传输协议 (HTTP-Hypertext transfer protocol) 是一种详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。
https协议需要到ca申请证书,一般免费证书很少,需要交费。http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。http的连接很简单,是无状态的HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议 要比http协议安全HTTPS解决的问题:1 . 信任主机的问题. 采用https 的server 必须从CA 申请一个用于证明服务器用途类型的证书. 改证书只有用于对应的server 的时候,客户度才信任次主机2 . 防止通讯过程中的数据的泄密和被窜改
GET- 从指定的资源请求数据。
POST- 向指定的资源提交要被处理的数据
JVM的堆是Java对象的活动空间,程序中的类的对象从中分配空间,其存储着正在运行着的应用程序用到的所有对象。这些对象的建立方式就是那些new一类的操作,当对象无用后,是GC来负责这个无用的对象
GC的工作目的很明确:在堆中,找到已经无用的对象,并把这些对象占用的空间收回使其可以重新利用.大多数垃圾回收的 算法思路都是一致的:把所有对象组成一个集合,或可以理解为树状结构,从树根开始找,只要可以找到的都是活动对象,如果找不到,应该被回收了。
Web 流:也被称为 Hybrid 技术,它基于 Web 相关技术来实现界面及功能:PhoneGap
虚拟机流:通过将某个语言的虚拟机移植到不同平台上来运行 Android
代码转换流:将某个语言转成 Objective-C、Java 或 C#,然后使用不同平台下的官方工具来开发
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>Window是一个窗口的概念,是一个抽象类,具体实现是PhoneWindow。
通过WindowManager来创建Window。
Window的具体实现位于WindowManagerService,WindowsManager和WindowMannagerService的交互是一个IPC的过程。
(1). 使用WindowManager添加一个Window的过程:
|
|
上面的代码将一个Button添加到屏幕坐标为(100,300)的位置.
(2).WindowManager.LayoutParams中的flag参数表示Window的属性,下面是几个比较常用的属性。
表示window不需要获取焦点,也不需要接收各种输入事件。此标记会同时启用FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层的具有焦点的window;
在此模式下,系统会将window区域外的单击事件传递给底层的window,当前window区域内的单击事件则自己处理,一般都需要开启这个标记;
开启此模式可以让Window显示在锁屏的界面上
(3).TYPE参数表示Window的类型,有三种,分别是应用Window,子Window和系统Window。
应用window对应着一个Activity,子window不能独立存在,需要附属在特定的父window之上,比如Dialog就是子window。系统window是需要声明权限才能创建的window,比如Toast和系统状态栏这些都是系统window,需要声明的权限是。
(4). window是分层的,每个window都对应着z-ordered,层级大的会覆盖在层级小的上面,应用window的层级范围是1~99,子window的层级范围是1000~1999,系统window的层级范围是2000~2999。
[注意,应用window的层级范围并不是1~999哟]
(5).WindowManager继承自ViewManager,常用的只有三个方法:addView、updateView和removeView。
(1).Window是一个抽象的概念,不是实际存在的,它也是以View的形式存在。在实际使用中无法直接访问Window,只能通过WindowManager才能访问Window。每个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系。
(2). Window的添加、删除和更新过程都是IPC过程,以Window的添加为例,WindowManager的实现类对于addView、updateView和removeView方法都是委托给WindowManagerGlobal类,该类保存了很多数据列表,例如所有window对应的view集合mViews、所有window对应的ViewRootImpl的集合mRoots等,之后添加操作交给了ViewRootImpl来处理,接着会通过WindowSession来完成Window的添加过程,这个过程是一个IPC调用,因为最终是通过WindowManagerService来完成window的添加的。
(1)Activity的window创建过程
1.Activity的启动过程很复杂,最终会由ActivityThread中的performLaunchActivity来完成整个启动过程,在这个方法内部会通过类加载器创建Activity的实例对象,并调用它的attach方法为其关联运行过程中所依赖的一系列上下文环境变量;
2.Activity实现了Window的Callback接口,当window接收到外界的状态变化时就会回调Activity的方法,例如onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent等;
3.Activity的Window是由PolicyManager来创建的,它的真正实现是Policy类,它会新建一个PhoneWindow对象,Activity的setContentView的实现是由PhoneWindow来实现的;
4.Activity的顶级View是DecorView,它本质上是一个FrameLayout。如果没有DecorView,那么PhoneWindow会先创建一个DecorView,然后加载具体的布局文件并将view添加到DecorView的mContentParent中,最后就是回调Activity的onContentChanged通知Activity视图已经发生了变化;
5.还有一个步骤是让WindowManager能够识别DecorView,在ActivityThread调用handleResumeActivity方法时,首先会调用Activity的onResume方法,然后会调用makeVisible方法,这个方法中DecorView真正地完成了添加和显示过程。
|
|
(2)Dialog的Window创建过程
1.过程与Activity的Window创建过程类似,普通的Dialog的有一个特别之处,即它必须采用Activity的Context,如果采用Application的Context会报错。原因是Application没有应用token,应用token一般是Activity拥有的。
(3)Toast的Window创建过程
1.Toast属于系统Window,它内部的视图由两种方式指定:一种是系统默认的演示;另一种是通过setView方法来指定一个自定义的View。
2.Toast具有定时取消功能,所以系统采用了Handler。Toast的显示和隐藏是IPC过程,都需要NotificationManagerService来实现。在Toast和NMS进行IPC过程时,NMS会跨进程回调Toast中的TN类中的方法,TN类是一个Binder类,运行在Binder线程池中,所以需要通过Handler将其切换到当前发送Toast请求所在的线程,所以Toast无法在没有Looper的线程中弹出。
3.对于非系统应用来说,mToastQueue最多能同时存在50个ToastRecord,这样做是为了防止DOS(Denial of Service,拒绝服务)。因为如果某个应用弹出太多的Toast会导致其他应用没有机会弹出Toast。
(1)Window的删除过程和添加过程一样,都是先通过WindowManagerImpl后,再进一步通过WindowManagerGlobal来实现的。
(2)WindowManagerGlobal.java中的removeView方法:
|
|
(3)removeViewLocked是通过ViewRootImpl来完成删除操作的。在WindowManager中提供了两种删除接口removeView和removeViewImmediate,它们分别表示异步删除和同步删除,其中removeViewImmediate使用起来需要特别注意,一般来说不需要使用此方法来删除Window以免发生意外的错误。具体的删除操作由ViewRootImpl的die方法来完成。在die的内部会判断是异步删除还是同步删除。在异步删除的情况下,die方法只是发送了一个请求删除的消息后就立刻返回了,这个时候View并没有完成删除操作,所以最后会将其添加到mDyingViews中,mDyingViews表示待删除的View列表。
WindowManagerGlobal.java中的removeViewLocked方法:
|
|
(4)具体的删除操作由ViewRootImpl的die方法来完成。 在die的内部会判断是异步删除还是同步删除。在异步删除的情况下,die方法只是发送了一个请求删除的消息后就立刻返回了,这个时候View并没有完成删除操作。doDie内部会调用dispatchDetachedFromWindow方法,真正删除View的逻辑在dispatchDetachedFromWindow方法的内部实现。
ViewRootImpl.java中die方法:
|
|
(5)ViewRootImpl.java中doDie方法:
|
|
(5)doDie方法中调用的dispatchDetachedFromWindow是真正删除View的逻辑。
在doDie方法中调用,实现真正的删除View的逻辑。在这个方法中主要做四件事情:
(1)垃圾回收相关的工作,比如清除数据和消息、移除回调。
(2)通过Session的remove方法删除Window:mWindowSession.remove(mWindow),这同样是一个IPC过程, 最终会调用WindowManagerService的removeWindow方法。
(3)调用View的dispatchDetachedFromWindow方法:
* 在内部会调用View的onDetachedFromWindow()以及onDetachedFromWindowInternal()。
* 对于onDetachedFromWindow()大家一定不陌生,当View从Window中移除时,这个方法就会被调用,
* 可以在这个方法内部做一些资源回收的工作,
* 比如终止动画、停止线程等。
(4)调用WindowManagerGlobal的doRemoveView方法刷新数据,包括mRoots、mParams以及mDyingViews, 需要将当前Window所关联的这三类对象从列表中删除。
ViewRootImpl.java中dispatchDetachedFromWindow方法:
|
|
(7)整体的调用关系是:
ViewManager –>> WindowManager(继承自ViewManager) –>> WindowManagerImpl(继承自WindowManager) –>>WindowManagerGlobal(WindowManagerImpl内部的一个对象) –>> ViewRooImpl.die(ViewRooImpl是WindowManagerGlobal的removeView方法中的一个对象) –>> doDie(die中的一个方法调用,判断异步还是同步删除) –>> dispatchDetachedFromWindow(在doDie方法中调用,真正用于删除View的逻辑) –>> 通过Session的remove方法删除Window(IPC过程) –>> WindowManagerService.removeWindow –>> dispatchDetachedFromWindow(这个是子View的dispatchDetachedFromWindow方法) –>> onDetachedFromWindow和onDetachedFromWindowInternal(都是子View中的) –>> WindowManagerGlobal.doRemoveView
(1)从WindowManagerGlobal的updateViewLayout方法看起:
首先它需要更新View的LayoutParams并替换掉老的LayoutParams,接着再更新ViewRootImpl中的LayoutParams,这一步是通过ViewRootImpl的setLayoutParams方法来实现的。在ViewRootImpl中会通过scheduleTraversals方法来对View重新布局,包括测量、布局、重绘这三个过程。除了View本身的重绘以外,ViewRootImpl还会通过WindowSession来更新Window的视图,这个过程最终是由WindowManagerService的relayoutWindow()来具体实现的,它同样是一个IPC过程。
|
|
(2)整体的调用关系是:
ViewManager –>> WindowManager(继承自ViewManager) –>> WindowManagerImpl(继承自WindowManager) –>>WindowManagerGlobal(WindowManagerImpl内部的一个对象) –>> updateViewLayout(WindowManagerGlobal中的一个方法) –>> setLayoutParams(updateViewLayout方法中调用) –>> ViewRootImpl.setLayoutParams(ViewRootImpl是updateViewLayout中的一个对象) –>> scheduleTraversals方法(在setLayoutParams中调用,在ViewRootImpl中) –>> WindowSession(在ViewRootImpl中,是一个Binder) –>> WindowManagerService.relayoutWindow(具体实现,更新Window的视图,IPC)
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>ContentProvider是一种内容共享性组件,它通过Binder向其他组件乃至其他应用提供数据。
首先来看一下流程图:
一个应用的入口方法为ActivityThread的main方法,该方法是一个静态方法,在main方法中会创建ActivityThread的实例并创建主线程的消息队列,然后在ActivityThread的attach方法中会远程调用AMS的attachApplication方法并将ApplicationThread对象提供给AMS,ApplicationThread是一个Binder对象,它的接口是IApplicattionThread,它主要用于ActivityThread和AMS之间通信。
访问ContentProvider需要通过ContentResolver,它是一个抽象类,通过Context的getContentResolver方法获取的实际上是ApplicationContentResolver对象,该类继承了ContentResolver并实现了ContentResolver中的抽象方法。通过ContentProvider的四个方法中任何一个都可以触发ContentProvider的启动。这里选择query方法。
ContentProvider的query方法首先会获取IContentProvider对象,最终会通过ApplicationContentResolver的acquireProvider方法,该方法没有做任何逻辑,直接调用了ActivityThread的acquireExistingProvider方法
|
|
上面代码中首先会从ActivityThread中查找是否已经存在ContentProvider,如果存在就直接返回,ActivityThread中通过mProviderMap来存储已经启动的ContentProvider对象,该容器的声明如下:
|
|
如果不存在就发送一个进程间请求给AMS让其启动,再通过installProvider方法来修改引用计数。
AMS是通过startProcessLocked方法来完成进程的启动,内部主要通过Process的start方法来完成进程的启动,新进程启动后的入口方法为ActivityThread的main方法:
|
|
在该方法中首先会调用ActivityThread的实例并调用attach方法进行初始化,然后进行消息循环,attach方法会将ApplicationThread对象通过AMS的attachApplication方法跨进程传给AMS,最终完成Contentprovider的创建。
|
|
APS的attachApplication方法调用了attachApplicationLocked方法,该方法又调用了ApplicationThread的bindApplication方法
|
|
ApplicationThread的bindApplication方法发送一个BIND_APPLICATION类型定位消息给mH,mH收到消息后会调用ActivityThread的handleBindApplication方法,该方法就完成了Application的创建以及Contentprovider的创建,可以分为四步:
1 创建ContextImpl和Instrumentation
|
|
2 创建Application对象
|
|
3 启动当前进程的ContentProvider并调用其中onCreate方法
|
|
installContentProviders完成的ContentProvider的启动工作,首先会遍历当前进程的ProviderInfo的列表,并一一调用installProvider方法来启动他们,然后将启动的ContentProvider发布到AMS中,AMS会把他们存储在ProviderMap中。
|
|
下面来看一下COntentProvider对象的创建过程,在installProvider方法中有下面一段代码,通过类加载器完成对象创建
|
|
上面代码还会调用COntentProvider的attachInfo方法来调用他的onCreate方法
|
|
到此为止onCreate方法已经被调用,意味着ContentProvider已经启动完成。
4 调用Application的onCreate方法
经过上面四个步骤,ContentPorovider已经成功启动,然后其他应用就可通过AMS来访问这个ContentProvider,拿到该对象之后就可以通过它所提供的接口来访问它了。
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>最近越来越多的人开始谈论架构。我周围的同事和工程师也是如此。尽管我还不是特别深入理解MVP,但是还是觉得比较牛逼,然后呢也想在公司的项目中去使用它。
目前网络上也有一些针对Android的快速开发框架,下面介绍3个主要的快速开发框架。针对这些快速开发框架,个人认为可以参考,但并不推荐使用,因为App整体依赖一个个人维护的框架风险实在太大,框架存在一些学习成本,本身也不一定完全符合App的需求,使用后可能存在代码的臃肿,还有就是架构限制。
Afinal
GitHub项目地址:Afinal
Afinal是一个Android的IOC,ORM框架,内置了四大模块功能:FinalAcitivity, FinalBitmap, FinalDb, FinalHttp。通过FinalActivity,可以通过注解的方式进行绑定UI和事件。通过FinalBitmap,可以方便的加载Bitmap图片,而无需考虑OOM等问题。通过FinalDB模块,通过一行代码就可以对Android的SQlite数据库进行增删改查。通过FinalHttp模块,可以以Ajax形式请求Http数据。
然而项目从去年就没有人更新维护了,ioc框架很多人不太喜欢而且性能不好。
GitHub项目地址:xUtils3.0
xUtils3变化较多所以建立了新的项目不在旧版(github.com/wyouflf/xUtils)上继续维护, 相对于旧版本:
可以看出xUtils3对于快速开发是一个不错的选择。
MVC,MVP还是MVVM?
越高级的模式复杂性越高,实现起来也越难。然后搭建项目时也是看项目的需求,别人说好你也有要实用才好,高效的实现项目的功能才是最好的架构模式。
那么,哪一个才是最好的呢?
个人觉得适合你的才是最好的,不要去盲目的跟风,大家说mvp好那你就使用咯,没有实践就没有话语权,所以说用哪种架构模式本人不发表任何意见:任何模式的动机都是一样的,那就是如何避免复杂混乱的代码,让执行单元测试变得容易,创造高质量应用程序,开发维护更高效。
在实际项目中思考架构时,也不会想着要用哪种模式,我只思考现阶段,以现有的人力资源和时间资源,如何才能更快更好地完成需求,适当考虑下如何为后期扩展或重构做准备。
这是我上一个项目的包架构:
当然咯,是按功能分的包,项目的功能不一样然后分包也不一样,但是基本大同小异。
所以确定架构分包的时候那就按你的需求来咯。
从上面可以看出:架构分包的时候我们包括逻辑功能和基础功能(通用功能)。
日志管理系统(LogManager)
不管哪个项目都需要自己的一套日志管理,一是为了生产调试时能更加高效的查看过滤日志,二是为了打包发布的时候用开关控制日志是否打印。 (我的日志用的是凯子哥的:Klog)
作用:当程序遇见异常情况时我们能够自定义异常处理,二是程序对不同的机型有不同的反应,那么测试时候可能没有发现但是我们可以把捕获的crash上传到服务器,便于异常收集和bug修复。
utils(工具类)
根据你的项目需求来合理定制你的工具类,将会对你的项目开发速度有很大的提升(反馈,版本校验更新你肯定能够用到)
看下我上个项目的工具类:
这功能是绝对项目中需要的,别告诉我你的项目还没有适配安卓6.0,适配了就肯定会有权限管理,我这里用的是 安卓6.0权限处理在项目中的实践,也还可以吧,反正github上的权限管理的开源东西比较多,觉得合适就ok。
哈哈,这样你的基础功能都搭建好了,然后就是一些逻辑功能的封装了。
1.封装自己的application和baseActivity类,最大可能的节省代码,加入mvp的思想来架构。
2.选择自己喜欢的网络请求框架并且适当合理的进行封装,加快开发的效率。
3.针对带有滚动控件嵌套有可能产生的滑动冲突,或者显示不全我们优先自定义一下viewpager,listview,gridview等。
4.封装listView或者recyclerView打造万能的适配器,觉得翔哥的封装的不错 打造万能的适配器。
5.一般的网络数据格式是json(我们就逗:普通数据json,刷卡交易数据xml),所以呢我json格式的用gson封装一下,xml格式暂时用的是pull解析后bean对象封装。
6.数据库的封装,对数据苦要求不高的话可以用原生的简单封装一下curd就好了,要求高点的话那就用第三方的好了。
Glide:相比较UIL,glide可以支持gif和短视频,支持与activity,fragment,application生命周期的联动,支持 okhttp、Volley
Fresco:三级缓存牛逼,对多帧动画图片支持更好,如 Gif、WebP
UIL:老牌的虽然不再更新维护,但功能强大
根据你的项目需求选择,熟悉UIL就用它,个人推荐Glide
okhttp:
okhttp是高性能的http库,支持同步、异步,而且实现了spdy、http2、websocket协议,api很简洁易用,和volley一样实现了http协议的缓存。
retrofit:
简化了网络请求流程,同时自己内部对OkHtttp客户端做了封装,同时2.x把之前1.x版本的部分不恰当职责都转移给OkHttp了(例如Log,目前用OkHttp的Interceptor来实现)
volley:
volley是一个简单的异步http库,仅此而已。缺点是不支持同步,这点会限制开发模式;不能post大数据,所以不适合用来上传文件。
个人建议使用retrofit,volley的通用性不高(资料最多)。
主要用来消息/事件的传递,却能实现组建之间的解耦。
eventBus3.0和otto都是使用注解的方式(@Subscribe、@Produce)来标注方法,Otto更多的使用场景是在主线程中,相对是轻量级的。
如果你对是不是轻量级不关心的话,我觉得两个差不多,但是还是很多人推荐使用otto。
butterknife8.0: https://github.com/JakeWharton/butterknife
在任何项目中使用butterknife都是正确且没有问题的. 非常轻量级的库,原因是性能高节省代码,而且不是你们所想的反射机制实现的。
Dagger2:它是不具有动态性的(使用时完全不使用反射)但是生成的代码的简洁性和性能都是与手写的代码同水准的。
2个都是很棒的,你可以选择额。
LitePal:LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,LitePal很“轻”,jar包只有100k不到,使用起来也比较简单,源码地址为LitePal地址,郭神开发的就是牛。
greenDAO:greenDAO与LitePal不同,其原理不是根据反射进行数据库的各项操作,而是一开始就人工生成业务需要的Model和DAO文件,业务中可以直接调用相应的DAO文件进行数据库操作,从而避免了因反射带来的性能损耗和效率低下。但是由于需要人工生成model和DAO文件,所以greenDAO的配置就略显复杂。
greenDAO用起来繁琐但是效率高点,LitePal用起来简单,所以你自己选择吧,个人还是觉得LitePal好用点。
ASimpleCache:ASimpleCache 是一个为android制定的 轻量级的 开源缓存框架。轻量到只有一个java文件(由十几个类精简而来)。
哈哈项目需要的基本架构需要的开源库都有了,你可以放心的开发了。
其实架构并不是那么难,也不要别人说怎么好就怎么干,你要相信总有一个东西是适合你的,打个比喻app架构就是盖房子,砖少就盖矮点吗,但是必须保证得结实,就像 框架不一定要强大但是必须健壮具有扩展性。
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>本文将介绍BroadcastReceiver的工作过程,主要包含两个方面的内容,一个是广播的注册过程,一个是广播的发送和接受过程。
首先看下流程图:
广播注册分为静态和动态注册,静态注册是由PMS(PackageManagerService)来实现的,除了广播其他三大组建都是应用安装时由PMS解析并注册的。
这里只分析广播的动态注册,动态注册过程是从ContextWrapper的registerReceiver方法开始的,ContextWrapper并没有实际工作,而是将注册交给了ContextImpl来完成。
|
|
ContextImpl调用自己的registerReceiverInternal方法
|
|
在上面代码中系统从mPackageInfo中获取IIntentReceiver对象,然后采用跨进成的方式向AMS发送广播注册的请求,之所以采用IIntentReceiver是因为注册是一个进程间通信的过程,IIntentReceiver是一个Binder接口,他的具体实现是LoadedApk.ReceiverDispatcher内部类,该内部类同时保存了BroadcastReceivr和InnerReceiver,这样当接受到广播时,ReceiverDispatcher可以很方便的调用广播的onReceiver方法。
广播的真正实现过程是在AMS中的,AMS的registerReceiver的关键只有下面一部分,最终会把远程的InnerReceiver对象以及IntentFilter对象存储起来。
|
|
这样整个广播的注册过程就完成了。
流程图:
当通过send方法来发送广播时,AMS会查找出匹配的广播接受者并将广播发送给他们处理。
广播发送有几种类型:普通广播、有序广播、粘性广播。这里只分析普通广播的实现。
广播的发送开始于ContextImpl的sendBroadcast,源码如下:
|
|
ContextImpl几乎什么都没干,直接向AMS发起一个异步请求发送广播。下面是AMS对广播发送过程的处理。AMS的broadcastIntent的方法源码如下:
|
|
broadcastIntent方法又调用了broadcastIntentLocked方法。该方法的开始有这一行代码:
|
|
这表示广播不会发送给已经停止的我应用,从Android3.1开始已经具有这种特性,这是因为在Android3.1中为Intent添加了两个标记,分别是FLAG_EXCLUDE_STOPPED_PACKAGES和FLAG_EXCLUDE_STOPPED_PACKAGES用来控制广播是否对停止应用起作用。
表示包含已经停止的应用,这个时候广播会发送给已经停止的应用
表示不包含已经停止的应用,这个时候广播不会发送给已经停止的应用。
从3.1开始默认为所有广播添加了FLAG EXCLUDE STOPPED _ PACKAGES标志。
处于停止状态分为两种情形:
从Android3.1开始处于停止状态的应用同样无法接收到开机广播。
在broadcastIntentLocked内部会根据intentfilter查找匹配的广播接受者,将满足条件的广播添加到BroadcastQueue中。
|
|
接着我们来看BroadcastQueue的scheduleBroadcastsLocked方法实现。
|
|
该方法并没有立即发送广播,而是发送了一个BROADCAST_INTENT_MSG类型的消息。当BroadcastQueue收到消息之后会调用processNextBroadcast方法。该方法对普通广播的处理如下:
|
|
可以看到无序广播存储在mParallelBroadcasts中,系统会遍历该集合并发送给他们的处理者,具体的发送过程是通过deliverToRegisteredReceiverLocked方法来实现,该方法将一个广播发送给一个特定的接受者,它的内部调用了performReceiveLocked方法来完成具体的发送过程,实现如下,由于广播会调起应用程序,因此app.thread不为空app.thread指ApplicationThread。
|
|
该方法会调用ApplicationThread的scheduleRegisteredReceiver,他的实现比较简单,通过InnerReceiver来实现广播的接受。InnerReceiver的performRecerver方法会调用LoadedApk.ReceiverDispatcher的performReceive方法。
|
|
在上面代码中会创建一个Args对象并通过mActivityThread的post方法来执行args中的逻辑,而args实现了Runnable接口。mActivityThread是一个Handler,它其实就是ActivityThread中的mH,mH的类型是ActivityThread的内部类H。在args的run方法中有如下几行代码:
|
|
很显然这个时候BroadcastReceiver的onReceiver方法被执行了,也就是说应用已经接收到广播。
到这里这个广播的注册,发送和接受过程已经分析完了,读者应该对广播的整个过程有了一定的理解。
未经许可不得转载,转载请注明zilianliuxue的blog,本人保留所有版权。
]]>