四大组件部分
一、Activity
参考博客:https://blog.csdn.net/geofferysun/article/details/52820089
1)生命周期:onCreate()、onStart()、onResume()、onPause()、onStop()、onDestroy();
●启动:onCreate()-->onStart()-->onResume(),(注意:onStart() 可见但在后台(无法交互),而onResume() 可见在前台(可交互));
●Home键:onPause()-->onStop();再次返回:onRestart()-->onStart()-->onResume();退出:onPause()-->onStop()-->onDestroy();
●如果Activity使用透明主题则不会调用onStop(),并且内存不足时也不会调用onStop(),所以保存信息的工作应该在onPause();
●旧Activity要完成onPause()后新Activity才可以onResume(),所以除onDestroy()以外都不要做耗时操作;
●异常销毁(内存不足时):系统会调用onSaveInstanceState()保存Activity状态,可以从onCreate()中取出所保存的Bundle对象;
●Activity优先级:前台(正在交互的)>可见(可见但不能交互)>后台(已暂停的),因此后台长期任务运行在Service中可保证持久优先级;
●startactivityForResult:此方式启动Activity可获得目标的返回结果;
●Activity四种状态:running、paused、stopped、killed;
2)Acitivity通信
●Intent.putExtra(),其中隐式Intent 需要通过解析后将Intent映射传递给处理者,解析机制就是查找Manifest中所有IntentFilter及其中定义的Intent;
●putExtra() 源码也是通过new 一个Bundle,再Bundle.putString(key, value),所以使用Bundle更方便传递,而不是直接用Intent来put 键值对;
●Bundle可以传递java的基本数据类型,如果是对象,先使用Serializable和Parcelable将对象序列化,再Bundle.putSerializable();
●Serializable和Parcelable区别:Parcelable安卓特有、效率高、适合在使用内存时、不适合数据将存储在磁盘(外界变化情况下无法保证数据持续性),Serializable是Java自带,使用了反射,会产生大量变量引起频繁GC,效率低、但时候在关于磁盘存储中;
●EventBus :使用第三方框架也可以解决组件间的数据传递、通信;
3)Acitivity启动模式(4种)
●standard(默认|标准) :每次都新建一个实例;
●singleTop(栈顶复用) :在栈顶就拿来用,onNewIntent()会被调用,否则新建实例;
●singleTask(栈内复用) :在栈内存在实例就把在它之上的都移除,调onNewIntent()将其复用,使用方法需要设置taskAffinity属性值;
●singleInstance(单实例):加强版singleTask,会创建一个栈单独复用(独享任务栈,使用比较频率比较少);
●启动模式的使用方式:Mainfest中指定lauchMode,或通过Intent.addFlags()标记位设置 ,两种同时使用时后者的优先级高于前者;
4)任务栈
●任务栈:非Activity类型是没有任务栈(即前台、后台任务栈)的,需指定标记位;taskAffinity为任务“关联”属性,需要与singleTask配合使用,它不能和包名相同;
●非单一性:不同应用的Activity可以在同一个任务栈,同一个应用的不同Activity也可以在不同任务栈,
●退出应用:需安全保存任务栈且清空任务栈后再退出
5)IntentFilter 过滤器匹配
一个Activity可以有多<intent-filter>,但一个Intent只能匹配一个<intent-filter>,其中有三个关键字段:action、category、data,(category会默认添加)
●action:"动作",区分大小写、有一个匹配成功即可;
●category:“种类”,可以不写(则使用默认的DEFAULT),但写了一个或多个,则必须要完全匹配;
●data:有一个匹配成功即可,它包含两部分“mineType”和“URI”,前者为媒体类型,后者是URI
●在使用隐式Intent前,调用resolveActivity 判断是否有可匹配的Activity,可避免报错“无法找到Activity”
5)Scheme跳转协议(类似路由选择)
页面内跳转协议,可通过自定义Scheme协议,根据URL来动态选择跳转的页面(类似Uri原理),使用场景:
●服务端下发URL根据Scheme协议做页面跳转、H5通过URL让页面跳转、APP之间通过Scheme协议跳转;
二、Service
参考博客:http://www.cnblogs.com/lwbqqyumidi/p/4181185.html https://blog.csdn.net/u014142287/article/details/50767412
它是一个服务运行于后台且不可见的组件,Service还可以与多个其他组件绑定交互,甚至是跨进程通信(IPC),但是后台不等于子线程!因此它是运行于主线程
1)创建方式
●继承自Service,重写onCreate()(只在创建时执行一次)、onStartCommand()(可执行多次,但bind启动时该方法不会执行)、onDestroy()方法,并在Mainfest中声明Service,最后通过显式或者隐式Intent构建Service意图。
2)启动方式(两种)
●startService(Intent):与启动它的Client 生命周期无关,且无绑定关联,所以重写的onBind()返回的是null,通过stopService()或Service内部stopSelf()来停止。同一类型的Service实例永远只存在一个。
●bindService(3个形参):Client与其绑定并通信,过程:Service先定义Binder对象以及公共方法给客户端调用,重写onBind()并返回该Binder对象,Client实现ServiceConnection接口获取Service的Binder对象,通过该对象就可以调用Service端的公共方法,实现了通信。Client在onDestroy()时候需unbindService()来与其解绑。
●startService()+bindService():同时用两种启动也可以,那要销毁Service就需要stopService()+unbindService(),前者停止启动后者解除关联。
3)其他特点
●Service默认是在主线程中,所以避免耗时操作引起的ANR;
●Remote Service 远程服务:将Service设为remote则单独在一个进程,就与Client不在同一进程,此时要关联绑定需要通过IPC机制,否则直接关联会报错;Local Service则为本地服务,同属一个线程。
●前台Service:通过常驻通知栏的形式,获得最高优先级以此保活,用startForeground()来设置;
●IntentService 异步线程服务(会自动停止) :内部由HandlerThread+Service组成,可执行耗时操作且避免阻塞主线程(它可启动多次但实例只有1个),它将intent加入队列,在新线程中一个一个处理,而不是真正意义上的多线程并发;继承此类内部onBind()直接返回null,所以不要用bind启动,然后重写onHandleIntent()并进行异步操作,结束后它会自动停止无需人为调用。
●HandlerThread 异步线程(源码只有150行):内部建立了Looper循环机制的Thread线程类(因为线程需要自己去创建looper),所以谷歌提供了一个封装好的,它内部就是handler+thread+looper,而且它又是一个线程类,所以可以在handlerMessage方法中执行异步任务。
它的特点:不会堵塞UI线程,使得子线程有自己的消息队列减少了性能消耗,缺点:对于网络IO和并发处理它并不适合(内部只有一个Thread),因为它是基于消息队列的串行处理,需要等待执行所以效率较低。
●广播不可以bindservice():因为广播生命周期很短,不适合使用绑定关联的形式,只能使用直接启动形式
4)IPC进程通信
●在下面的文章会分析
三、BroadcastReceiver
参考:https://www.jianshu.com/p/ca3d87a4cdf3 https://blog.csdn.net/nzfxx/article/details/51835743
广播使用了观察者模式:基于消息的发布 / 订阅事件模型,三个角色:广播发送者、广播接收者,消息中心(AMS);它通过Binder向AMS进行注册,发送者通过Binder把广播发给AMS,AMS再去查找符合条件的的注册者,将广播发送发送到相应的循环队列中,消息循环执行拿到此广播,再回调onReceive();
1)订阅过程
继承BroadcastReceivre,复写onReceive()实现接收广播后的动作,运行于UI线程不可耗时操作;
●静态注册:常驻监听广播形式(进程杀死仍然可接收到广播),在Mainfest中声明<receive>标签,intent-filter中设置action注册监听的广播
●动态注册:实例化BroadcastReceivre对象,再实例化 IntentFilter对象并addAction()添加监听的action事件,最后调用registerReceiver()注册,需要手动的注销unregisterReceiver();
●动态广播最好在Activity 的 onResume()注册、onPause()注销,这样可以避免内存泄漏;
2)发布过程
通过Intent构建广播类型,setAction()设置action事件等,最后sendBroadcast(intent)向AMS发送;
●广播的类型
1. 普通广播:若发送的广播有相应权限,那么接收者也需要相应权限,否则收不到;
2. 系统广播:接收者只需注册action,不需手动发送,由系统自动发送广播;
3. 有序广播:通过sendOrderedBroadcast(intent)发送有序广播,接收者根据优先级大的先接收,且可修改、拦截广播;
4. 本地广播(内部基于Handler实现):只有自己app可接收到,其他app接收不到,普通广播默认是全局的,有安全隐患和性能消耗,可exported设为false,并增加相应permission用于权限校验,还可以指定发送的包名setPackage()以增加本地安全性,它内部由Handler实现,相比Binder机制的效率要高;
5. 粘性广播:Android5.0 & API 21中已经失效,也就是后来注册的也能接收到之前发送的广播;
●最终接受者:针对有序可设定一个最终接受的 final receiver;
●OnReceive(context,intent)中不同的广播类型返回的context是不一样的,需要注意一下
四、ContentProvider (内容提供者)
参考:https://www.jianshu.com/p/ea8bc4aaf057
https://blog.csdn.net/prince58/article/details/51794393
ContentProvider底层使用Binder机制进行跨进程通信(基于匿名共享内存机制),它对数据源(SQLite数据库、文件、XML等)进行抽象封装,避免了直接开放数据库带来的安全问题, 对外统一了存储和获取的接口;
Provider像一个搬运工、中间者,外部通过Uri找到对应的Provider,告诉它要增删改查,它就去对数据源进行操作并把结果返回给外部。
1)工作原理:
所有的ContentProvider(供应者)都由一个ContentResolver(分解器)统一管理,外部进程通过Uri再由Resolver找到对应的Provider,Provider再根据Uri返回的MIME类型去操作SQL,Uri也就是具体数据库的一个映射。
2)三个工具类
●ContentUris:操作Uri,对Uri追加id,或者Uri中获取id;
●UriMatcher:在ContentProvider 中注册URI,或根据 URI 匹配 ContentProvider 中对应的数据表;
●ContentObserver:观察者模式观察Uri引起 ContentProvider 中的数据变化(增、删 & 改)则通知外界,所以需要注册和注销监听;
3)使用过程
数据源以SQLite为例,Provider对外部提供“增删改查”的接口
●进程内通信:
1、创建数据库类;
2、自定义 ContentProvider 类(使用UriMatcher注册Uri,实例化DBHelper、SQLiteDatabase数据库,提供对外的增删改查的API,UriMatcher根据传入Uri决定操作的表);
3、注册创建的 ContentProvider类(Maifest中注册<provider>);
4、进程内访问 ContentProvider的数据(通过ContentValues.put()构建插入实体,设置Uri,获取ContentResolver就可以增删改查)
●进程间通信:
前两步骤与上面的一样,主要是第三步和访问时有区别
3、注册创建的 ContentProvider类(<provider>中需声明进程访问权限、自定义的读写权限(外部也需要同时声明)、exported设为true允许其他进程使用)
4、外部进程应用声明如上相同权限
5、访问 ContentProvider的类(与进程内一样,设置Uri,构建ContentValues插入实体进行,获取获取ContentResolver进行增删改查)
注意:在时候query查询时候,返回的是Cursor游标,通过moveToNext()判断是否还有实体,使用完毕要cursor.close();
4)注意事项
●设置signature级别的permission可对同一签名的其他应用开放
●设置path-permission可开放部分Uri
●设置android:multiprocess为false则Provider为单例模式
五、Fragment
参考:https://blog.csdn.net/a31081314/article/details/53332310 ,它可以算得上第五大组件了。
1、基本用法:可以把它当作一个ViewGroup、或者自定义组件来使用,但是它却有自己的生命周期而且依赖于Activity
●静态添加:在XML中使用Fragment组件,与自定义组件的使用方式相同
●动态添加:XML中使用FrameLayout或其他ViewGroup占位,java中new 出Fragment实例,然后在Activity中get 获取到FragmentManager,通过Manager的beginTransaction()开启事务管理,再由事务对象transaction来进行对应的操作,如添加add()、替换replace()等等Api,这里需要传入替换掉ViewGroup,最后commit()提交事务;
2、生命周期(它依赖于Activity的生命周期,它的生命周期穿插在Activity生命周期之中)
Fragment 被启动时(在Activity的onStart()开始时才执行):onAttach()-->onCreate()-->onCreateView()-->onActivityCreated() 。即:Fg与Ac关联-->Fg第一次创建-->Fg创建View视图(返回根视图)-->Ac的onCreate()执行完毕;
Fragment 交互过程(与Activity的对应周期交替执行):onStart()-->onResume()--> 在前台交互。例如:Fg执行完onStart()紧接着Activity的onStart()也执行完毕,然后Ac执行on Resume();当Fragment暂停时:在前台交互 --> onPause()-->onStop()。这个过程与交互前相反,先执行Fragment的再去调Activity对应的周期。
Fragment 被销毁时(在Activity onStop()后被调用):onDestroyView()-->onDestroy()-->onDetach() -->调 Activity的onDestroy()
●总结:Fragment生命周期是穿插在Activity生命周期之中,Ac的onStart()开始执行时就会去调Fg启动方法,一直到Fg的onStart()结束,Ac的onStop()后也多了3个周期方法,这是一种对称关系,即: 关联-->创建-->创建视图 。。。销毁视图-->销毁-->解绑, 而在前台交互过程中,Fg与Ac的周期方法是间接执行的。onCreateView()中会返回根视图,即通过Inflater加载xml布局文件,第三个参数要传false,最终返回Viwe对象,所以在重写这个方法进行视图绑定。
3、与Activity交互和通信
●Fg调Ac:getActivity() 获得宿主Activity的对象,通过实例来操作,但要考虑Activity异常销毁时getActivity()会报空指针的情况;
●Ac调Fg:接口回调 ——定义接口,Fg中提供外部调用的Api,并设定由实现了接口的监听者去调用这些Api,Ac中创建接口的监听者,在重写接口方法中完成自己要回调的具体过程,在创建Fg时传入监听者,这就是接口回调的过程。
●Fg调Fg:Fg之间没有任何依赖关系,都是通过Activity作为中介(其实是FragmentManager),由Manager通过findFragmentByTag()或findFragmentById()得到Fragment 对象,再由对象去调用它的public方法;或Fragment之间通过接口回调传递数据,在宿主Activity中传入各自的回调对象即可。
●使用EventBus传递数据对象:需要Fg先订阅后才会收到事件消息,所以对于创建Fg时就发送,需要使用粘性事件,或者在创建Fragment后eventBus.register(Fragment) 进行注册监听,这样在使用事务做Fragment替换或添加时,目标Fragment可以收到事件。
● 使用Bundle传递对象(需序列化):通过 setArguments(bundle)发送;在Fragment的onAttach()中 getArguments()获得bundle;
4、FragmentManager 与 回退栈
1)FragmentManager:每个宿主Activity (继承自FragmentActivity) 和 Fragment 在创建时都会初始化一个FragmentManager对象,即FragmentManager 和ChildFragmentManager,用于对Fragment事务进行管理。
●getFragmentManager() 和 getsupportFragmentManager区别:都是获取到Activity中的FragmentManager,前者在安卓3.0系统之后的新增的,后者是在support V4 包中,在使用Fragment时要注意引用的是app的还是v4包中的,再获取Manager时也就要获取对应包中的。
●getFragmentManager() 和 getSupportFragmentManager() 区别:前者获得 Fragment 所在父容器的Manager,通常都是宿主Activity中的Manager;后者所获得的是Fragment 中嵌套的那个Fragment 的Manager。
●FragmentManager()总结:只有两种类型的FragmentManager,即Activity和Fragment的,Fragment所有操作比如替换、隐藏和加入栈,都是通过事务进行提交,而事务又是由Manger获取到的,所以不同的Manager保存的Fragment对象的状态是不一样的,在使用时,只要确保获取到同一个FragmentManager,也就是同一个管理者,就可以保证操作的一致性。
2)回退栈:Fragment的回退栈类似activity栈,如果不加入回退栈,fragmentA被fragmentB替换后,A的实例会被干掉,点击goback键不会回到A,fragment栈清空之后再点击回退键才会调用activity栈,回退到activity。
●Fragment没有加入栈时,按Back就是操作Activity的栈,当动态添加Fragment时,使用事务来将它加入返回栈中FragmentTransaction.addToBackStack(String),这样replace()时,已入栈的就不会再创建实例,但是它的View视图依然会销毁和重建,对于使用率比较多的fragment,可以使用hide()隐藏来避免执行生命周期,这样View视图也不会频繁的销毁和重建;另外还可以给回退栈增加监听器。
5、与ViewPager组合注意事项
●ViewPager+Fragment:这种可滑动的组合使用需用到adapter。其中FragmentPagerAdapter与FragmentStatePagerAdapter的区别:前者适用于界面较少的情况,后者则适用于界面较多的情况,因为:在他们的destroyItem()中,前者最终只是将Fragment的UI与Activity的UI进行解绑,而后者则是回收了内存;
●如果ViewPager是Activity内的控件,则传递getSupportFragmentManager(),如果是Fragment的控件中,则应该传递getChildFragmentManager()。只要记住ViewPager内的Fragments是当前组件的子Fragment这个原则即可。
6、Fragment返回监听
由宿主Activity重写onBackPressed(),每次按下返回都由宿主去判断Fragment的返回栈中是否为空,不为空则执行pop,否则finish()掉;另外一种方法是在Fragment中自己一个处理返回事件的方法,通过接口回调的方式,在返回事件触发后让宿主调用该方法,这样Fragment可以由自己去判断要不要消费事件,不消费则交给Activity去消费处理。
7、Fragment踩过的坑(注意事项)
https://www.jianshu.com/p/d9143a92ad94
●show(),hide() 不会调用Fg的生命周期,只是设置了View的setVisibility属性;而 replace()替换、add()添加、remove()移除 会调用生命周期; replace()是 add()和 remove()的合体,所以add() 和 replace()不要在同一个阶级的FragmentManager里混搭使用。
●getActivity()空指针:Activity异常销毁后调用该api会报错,可在Fragment的onAttach()中获得context实例再强转为Activity,这样有内存泄漏的隐患但比直接闪退的要好;在Fg中使用时候,可以先判断该实例是否为空。
●异常Can not perform this action after onSaveInstanceState :Activity在异常关闭时会调用onSaveInstanceState(),在onResume()之前的Fragment事务操作会报该异常,可以使用commitAllowingStateLoss()方法提交事务;或者在onActivityForResult()/onNewIntent()方法中去执行事务操作。
●重叠异常:在Activity异常关闭,再次回到界面可能会重叠绘制,需在Activity中判断savedInstanceState==null 时才进行创建Fragment实例;且在v4-24.0.0以下没有保存Fragment的mHidden属性,即使用show()和hide()后内存重启是默认show()的,但replace()不会,所以给fragment添加tag,在savedInstanceState != null 时 ,根据tag来做show()或者hide();另一种做法,子Fragment自己管理hidden属性,重写onCreate(),在Bundle不为空时getBoolean(),传入STATE_SAVE_IS_HIDDEN,根据返回的布尔值自己开启事务去调用hide();
●Fragment嵌套的问题:需要对getFragmentManager()和getChildFragmentManager()的正确使用;且v4 23.2 包以下有个bug,只有顶层的父Fragment才能在onActivityResult()中接收到返回值;
●移出栈:remove()可能无效,因为add() Fragment 时又addToBackStack(name)加入回退栈,此时remove()并不能被移除栈,只能用popBackStack();popBackStack() 加入队列末尾,等待其他任务完成再出栈,而popBackStackImmediate()是将队列内任务立即执行再将出栈任务放入队尾,即立即执行;多个Fragment同时出栈应该使用后者
●转场动画:使用setCustomAnimations(enter, exit)只能设置进场动画,第二个参数并不是出场动画的,所以应该用setCustomAnimations(进栈动画, exit, popEnter, 出栈动画),并重写onCreateAnimation()来控制出场动画,在动画执行中不要去执行事务。
8、Fragment代替方案(响应式UI)
APP使用单个Activity +多个Fragment的UI架构,或者是Fragment的多层嵌套,在提高性能的同时会引发一些问题,需要解决Fragment回退栈和内存重启时的问题;另一种UI架构模式是 多模块Activity+多个Fragment,按功能或业务模块划分使用Activity作为入口,模块下的子业务则用Fragment,业务不同就用Activity,这样的层次结构也很清晰,更方便维护。
Squre提出的“放弃Fragment吧”:Fragment在3.0被设计出来的初衷是解决大屏幕的UI适配,但随着它的Bug都与Fragment 的生命周期有关,我们可以选择一种代替方案:使用响应式 UI来代替Fragment, 通过add() View来创建视图,并实现回退栈以及处理屏幕事件,使用MVP架构将业务逻辑交给P层,这样Fragment其实也就是一个空壳,不用它也能满足实际开发的需求。(虽然View不能从startActivityForResult获得返回结果,但是我们可以使用其他手段解决这种数据传递问题,如EventBus)
SQLlite数据库
参考:https://www.jianshu.com/p/2398aad3bd61 https://www.jianshu.com/p/8e3f294e2828
SQLite是轻量、跨平台、开源的,每个数据库是以单个文件(.db)的形式存在,又是以B-Tree的数据结构形式存储在磁盘上。存储 结构型、关系型的数据,支持事务处理、在独立进程等特点;
两种操作方式:
●SQLiteDatabase为SQLite的封装类,提供了便捷API,我们通过SQLiteOpenHelper辅助类来操作它的增删改查,只需要调用API:insert()、update()、delete()、rawQuery();
●除“查询”以为,其他操作都可以直接使用SQL语句,最后通过db.execSQL(sql)来执行 ;
1)使用过程
创建数据库 & 操作数据库(增、删、查、改)
●自定义数据库子类(继承SQLiteOpenHelper类):构造法中定义数据库名和版本等,重写的onCreate()中:用execSQL()执行SQL建表语句;onUpgrade()中:版本升级才调用
●实例化SQLiteOpenHelper,并通过getWritableDatabase()获取“增删改”操作权限;getReadableDatabase()获取“查”权限操作,这两个都返回SQLiteDatabase 实例;
●插入:需构建ContentValues,再put键值对,最后sqliteDatabase.insert()插入;其他方法相类似,只需调用API传入参数;
●查询:query()返回Cursor 游标对象,Cursor指向的就是每一条数据,它有很有API;
●关闭:操作完需要close();
2)性能优化
●事务管理:数据库是磁盘操作,有多少条SQL就有多少磁盘操作,且不能保证所有数据都能同时插入,为提高效率和可靠性:开启事务,将批量的操作作为一次事务可减少磁盘读写,在失败时可回滚;
beginTransaction()开启事务,setTransactionSuccessful() 设置事务处理成功最后才会提交,endTransaction()关闭事务,
●索引:在onCreate()中db.execSQL(“创建索引的SQL”)即可;创建索引需性能开销,对查询频率高的可使用索引,所以有利也有弊;
●limit:分页查询
3)线程问题
大量数据处理时会使用多线程去操作数据库,某个close()会导致其他线程异常,可将SQLiteDatabase实例放入Application,让他们生命周期一样,或者使用计数器,为0时才关闭
SharedPreferences(接口)
SharedPreferencesImpl是SharedPreferences接口的具体实现类,一个name对应一个SharedPreferencesImpl,一个应用程序中根据name的不同会有多个SharedPreferencesImpl。
1)读写步骤
都是通过getSharedPreferences()获得SharedPreferences对象来进行操作,传入name和权限控制即可,同一个name获取到的是单例模式的同一个sp对象;
●写入:由sp对象内部类Editor来put()键值对,可存储基本类型(或将对象转换为String),最后commit()或apply()提交;(提交过程为:EditorImpl缓存一个Map,commit()时将缓存Map写入内存Map,再I/O磁盘操作写入XML文件(明文存储) );
●读取:由sp对象直接get(),传入key以及数据类型即可;
2)监听数据变化
基于观察者模式,sp对象可注册、注销监听者,实例化OnSharedPreferenceChangeListener 数据监听者,当数据发生变化会回调里面的方法;
3)权限控制(4种)
Activity.MODE_PRIVATE(默认私有的,写入内容会被覆盖)以及外部只可读、只可写、追加模式;
4)线程同步问题
●先启动主进程获取到sp对象,然后启动其他进程并获取sp对象,那么此时对sp的数值进行修改均不能对其他进程产生作用。(也就是不同进程获取到sp以后,修改了sp都需要重新获取sp对象才能完成“同步”)
● 3.0以上可用MODE_MULTI_PROCESS 模式进程共享sp,但6.0已被弃用,所以谷歌是不建议用sp进程共享;
5)注意
●在UI线程中调用getXXX可能会导致ANR
●sp只适合存储少量数据,如它的xml文件很大,初始化加载到内存中会很浪费
●SharedPreferences每次写入都是整个文件重新写入,不是增量写入
●commit:在调用线程中执行的写操作,从内存到硬盘的提交都是同步所以会阻塞线程,有返回结果boolean;
●apply:在子线程执行写操作,从内存提交时同步,内存到硬盘是异步,效率比commit高一点,但无返回结果;
●注意:不要通过多个Editor来写入,多个Editor会覆盖导致缓存Map为空,只有最后一个提交的才有效;
Context(环境、上下文)
Android有些组件并不像一个Java对象通过new 创建,而是要有它们各自的上下文环境Context;Context(抽象类)有两个子类:ContextWrapper(包装类)、ContextImpl(实现类);ContextWrapper 包装类又有三个具体的子类:Application、Activity和Service;因此,Application、Activity、Service都是Context。
1)context基本含义
●有三种context:Application、Activity 和 Service;
●Activity、Dialog中只能用Activity类型的Context;
●Context的总数 = Activity数量 + Service数量 + 1 (Application)
2)context的作用
一个程序就像一部电影,那么context就是一个摄影机给每个角色提供场景和镜头。Context的功能都是由ContextImpl 子类去具体实现的,它有很多功能,如弹出Toast、启动Activity、启动Service、发送广播、操作数据库等等,需要用到Context
3)Application
Application 不应该仅仅当做一个工具类来使用,它更应该作为一个context被使用
●获取方式:getApplication() 只能在Activity和Service中调用;getApplicationContext() 可在任何一个地方可用,都是获取到同一个context;
●getBaseContext() 是获取到一个ContextImpl对象,Context的具体功能都是由ContextImpl类去完成的:而包装类中的三种子类又是通过内部的mBase对象(ContextImpl的具体子类)去完成具体功能,因此证实了ContextImpl才是具体是实现;
●Application初始化顺序:构造法、attachBaseContext()、onCreate();其实mBase对象是在attachBaseContext()中才被赋值的,所以只能在此方法之后再调用Context中的API,千万不要在构造法中调用Context的API,否则会报错;
●自定义一个Application类时不需要再使用单例模式,因为它本身就是系统的单例;
4)其他注意点
●View.getContex() 或者 Activity.this:返回View所在的Activit对象或Activity实例本身,即Activity类型的context;
●context可能引起内存泄漏:不正确的单例模式(非线程安全的单例模式),或者View持有context引用,
Handler 异步消息处理(线程间通信)
参考:https://www.jianshu.com/p/e172a2d58905 使用步骤见参考链接
Handler消息机制解决了多个线程并发更新UI的同时保证线程安全(使用UI锁则会影响效率),Handler可继承Handler或匿名内部类完成复写;
1)发送方式两种:post()、sendMessage();
区别:本质上没有区别的,post也是用sendMessage实现,post传入一个Runnable(最终封装为Message对象),而sendMessage传入Message对象,前者不需外部创建消息对象且直接在run()中完成UI操作,后者要在handler的handleMessage回调中完成;
2)四个角色:Handler、Looper、MessageQueue、Message;
●Handler:消息发送者、处理者;
●Looper(使用了ThreadLoacl):主线程自动创建有一个Looper并Looper.loop()开启循环,子线程则需要手动通过Looper.prepare()创建Looper或Loop.getMainLooper()获取主线程的Looper;
●MessageQueue:消息队列(单链表)存储消息实体,先进先出;
●Message:消息实体,可在工作线程中实例化获取,它持有Handler发送者的引用;
注意:前三个(核心类)默认都是运行在主线程,Message可在工作线程;每个线程只能有一个Looper和多个Handler处理者,一个Looper也可以绑定多个处理者,但每个处理者只能绑定一个Looper,即:Looper可以1对多,Hnadler只能1对1;
3)工作流程:
●准备过程:主线程中需创建handler、looper、MessageQueue,looper对MessageQueue进行消息循环,此时handler已经和主线程的Looper、MessageQueue进行了绑定;
●消息入队:工作线程中通过主线程的handler实例,将Message发送到MessageQueue中,等待looper的消息循环;
●消息循环:Looper对线程进行阻塞式的消息循环,当有Message时则取出,Message中持有handler发送者的引用,Looper就将消息交给handler处理;
●消息处理:handler是消息的处理者,它接收Looper发来的消息并调用handleMessage()进行回调处理;
4)源码理解:
●准备(关联)过程:主线程会在main()中自动创建Looper对象,同时关联并创建MessageQueue对象,且自动loop()开启循环(子线程需手动通过Looper.prepare()创建,且需手动quit()退出消息循环,但主线程不可退出),因此没有Looper则无法与handler关联;
●消息循环:消息出队并分发给对应的Handler实例,dispatchMessage(msg)进行派发,并根据callback为空来判断是那种形式发送的,从而决定回调方式;
●创建消息:Message内部维护了1个Message池,用于Message消息对象的复用,通过obtain()从池内获取,可区别于new()方式消耗大量内存
●消息发送:Handler发送消息的本质 = 为该消息定义target属性(即本身实例对象) & 将消息入队到绑定线程的消息队列中
5)通信的原理:
同一个进程的不同线程可享用的同一片内存空间,线程可以通过公共内存空间(共享内存)来通信。事实正是如此,在 Android 的消息机制中,扮演这个特定内存空间的对象就是 MessageQueue 对象,发送和处理的消息就是 Message 对象。其他的Handler 和 Looper 都是为了配合线程切换用的。
6)内存泄漏:
Handler使用不当会导致内存泄漏,如匿名内部类和非静态内部类会持有外部类引用,即Handler持有Activity引用,在message中又持有handler引用,message在消息队列中需等待looper的调用,所以message会间接持有activity的引用,导致activity无法被销毁;
三种解决办法:
●使用静态内部类的Handler
●Handler持有外部activity的弱引用
●在Activity的onDestroy()中使用Handler.removeCallback(),移除回调即可释放外部引用
7)Looper 死循环(但不会卡死主线程):
参考:https://www.zhihu.com/question/34652589
●在主线程中会自动创建Looper并开启loop()执行死循环,但是它不会卡死UI线程,是因为:
Android应用程序的主线程在进入消息循环过程前,会在内部创建一个Linux管道(Pipe),这个管道的作用是使得主线程在消息队列为空时可以进入空闲等待状态,并且使得当应用程序的消息队列有消息需要处理时唤醒应用程序的主线程。
●从Linux系统来了解 Epoll机制(阻塞是有的,但是不会卡住):
1,epoll模型(当没有消息的时候会epoll.wait,等待句柄写的时候再唤醒,这个时候其实是阻塞的。)
2,所有的ui操作都通过handler来发消息操作。
比如屏幕刷新16ms一个消息,你的各种点击事件,所以就会有句柄写操作,唤醒上文的wait操作,所以不会被卡死了。
线程池
线程池继承自Executor(接口),具体的实现类为ThreadPoolExecutor,它的构造法有6个核心参数。java内置了4种线程池,我们也可以通过设置核心参数来完成自定义线程池;
线程池的好处:避免频繁的创建和销毁线程所带来的性能开销;
1)6个核心参数
●corePoolSize:核心线程数
●maximumPoolSize:线程池所能容纳的最大线程数
●keepAliveTime:非核心线程 闲置超时时间
●unit:指导keepAliveTime参数时间单位
●workQueue:任务队列
●threadFactory:线程工厂
优先级:核心线程> 任务队列 > 最大线程
2)4种内置线程池(Java已根据应用场景配置好核心参数)
●定长线程池(FixedThreadPool):只有核心线程 & 不会被回收、线程数量固定、任务队列无大小限制(超出的线程任务会在队列中等待);使用场景:控制线程最大并发数
●定时线程池(ScheduledThreadPool ):核心线程数量固定、非核心线程数量无限制(闲置时马上回收);使用场景:执行定时 / 周期性 任务;
●可缓存线程池(CachedThreadPool):只有非核心线程、线程数量不固定(可无限大)、灵活回收空闲线程(具备超时机制,全部回收时几乎不占系统资源)、新建线程(无线程可用时);使用场景:执行大量、耗时少的线程任务;
●单线程化线程池(SingleThreadExecutor):只有一个核心线程(保证所有任务按照指定顺序在一个线程中执行,不需要处理线程同步的问题);使用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作;
注意:内置4种线程池是通过Executors创建并返回ExecutorService对象;
3)使用步骤(4种内置线程池和自定义线程池的创建方式不同)
●自定义创建方式:new ThreadPoolExecutor()
●内置线程池创建:ExecutorService xxxxxThreadPool = Executors.newxxxxThreadPool();
●threadPool.execute( Runnable()) 提交一个Runnable到线程池中
●threadPool.shutdown() 关闭线程池;
AsyncTask(抽象类)
参考:https://www.jianshu.com/p/ee1342fcf5e7
内部由线程池+Handler组成(其中线程池为2个:任务队列线程池、执行线程池),解决了异步任务处理和线程间消息传递的问题,但是尽量不要使用它来完成特别耗时或者大量并发的任务,只使用线程池会更合适;
1)使用方法(AsyncTask<3个参数>,5个方法)
●创建 AsyncTask 子类 & 根据需求实现核心方法
●创建 AsyncTask子类的实例对象(即 任务实例)
●手动调用execute()从而执行异步线程任务(其他方法都不可以手动调用)
执行流程:、手动调用 execute()、doInBackground()、onProgressUpdate()、onPostExecute()、onCancelled();
方法依次为:执行、工作线程中异步、进度更新、结果返回、取消异步;
其中:onPreExecute()是在异步任务执行前调用、doInBackground()是在工作线程中执行、onCancelled()是在onProgressUpdate()中调用;
2)源码理解:
●执行任务前,通过任务队列线程池类(SerialExecutor,内部维护一个双端队列)将任务按顺序放入到队列中;
●通过同步锁 修饰execute()从而保证AsyncTask中的任务是串行执行的(也可以通过设置来改变为并行处理,但不推荐);
●然后交给执行线程池(THREAD_POOL_EXECUTOR)去执行异步任务,取出队列中的对象调用他们的call()。。。;
3)内存泄漏
因为内部使用了Handler,且使用线程异步处理,所以与Handler原理一样,会导致内存泄漏;可在Activity的onDestroy()中调用AsyncTask.Cancel();
Binder(IPC的底层实现)
参考:https://www.jianshu.com/p/4ee3fd07da14
一、Binder的理解
1)Binder概念: Binder是Android中实现跨进程通信的一种方式;Binder驱动则是连接Service进程、Client进程和Service Manager进程桥梁(代理者、中间者);Binder类则是代码中的具体表现,它实现了IBinder接口;
2) Linux的进程空间:一个进程空间= 用户空间 & 内核空间(Kernel),即把进程内的用户和内核隔离开来(进程隔离),只有内核空间才是共享空间,所有进程共用1个内核空间;所以Binder驱动是在内核空间,它给用户空间提供数据共享;
3)Binder模型:基于Client - Server 模式,Binder使用内存映射传递数据,数据只需拷贝1次,而传统的跨进程通信需拷贝数据2次(即用户空间A -->内核空间 -->用户空间B);
二、工作原理
1)4个角色(Binder驱动是主角,其他3者都没有直接连接,都是通过Binder驱动进行中间传递)
●Clinet进程(使用服务的客户端)、Service进程(提供服务的服务端)、ServiceManager(管理Service的注册与查询,类似路由器)、Binder驱动(连接前面3者并,数据通过内存映射传递、自身管理Binder线程池,持有Service进程在内核空间的Binder实体,并将引用提供给Client进程)
●前三者都是在不同进程中,都必须通过Binder驱动交互,并非直接交互,Binder驱动是一种虚拟设备驱动;
●Binder驱动和Service Manager进程 属于 Android基础架构(即系统已经实现好了);而Client 进程 和 Server 进程 属于Android应用层(需要开发者自己实现);
●Binder线程池:Server进程会创建很多线程来处理Binder请求,Binder驱动采用线程池对此进行管理,而不是由Server进程来管理的;(一个进程的Binder线程数默认最大是16,超过则阻塞并等待空闲,所以ContentProvider只能有16个线程同时工作)
●Binder机制的优点:数据只拷贝1次,对进程UID/PID身份校验、采用C/S模式,所以Binder机制相对于Linux中的管道、socket等进程通信方式,它更加的高效安全;
三、具体实现
1)Android的具体实现:主要依靠 Binder类,其实现了IBinder 接口,IBinder接口定义了远程操作对象的基本接口,代表了一种跨进程传输的能力;
●步骤1:注册服务:Server进程通过Binder驱动向 Service Manager进程 注册服务,注册服务后,Binder驱动持有 Server进程创建的Binder实体;
●步骤2:获取服务:Client进程 使用 某个 Service前,须通过Binder驱动 向 ServiceManager进程 获取相应的Service信息,此时,Client进程与 Server进程已经建立了连接;
●步骤3:使用服务:Client进程 根据获取到的 Service信息(其实是Binder的代理对象),由代理对象并通过Binder驱动协助完成了跨进程通信,即让客户端去调用服务端的方法,并返回结果。(理解为:C端只持有Binder代理对象的引用,代理对象与S端又使用了内存映射的方式完成数据传递)
2)代码实现
具体代码见链接中博客
●步骤1:注册服务:注册<Service>后,继承Binder类,创建Binder对象通过new Stub() 实例化它的子类,创建 IInterface 接口(该接口定义返回的Binder'对象)
●步骤2。。。去链接中看吧,太多了
AIDL
参考:https://www.jianshu.com/p/34326751b2c6
基于Binder机制的一种具体IPC实现,通过定义aidl接口文件快速生成对应的Binder类以及实现IBinder接口,最终以远程服务Service运行;
一、实现步骤
1)服务器端(Service)
步骤1:新建定义AIDL文件,并声明该服务需要向客户端提供的接口
步骤2:在Service子类中实现AIDL中定义的接口方法(即实例化AIDL的Stub类(Binder的子类)),在onBind()返回自己定义的Binder对象,接着与本地Service用法一样,在其他生命周期方法中写自己的逻辑;
步骤3:在AndroidMainfest.xml中注册服务 & 声明为远程服务(android:process=":remote" )
2)客户端(Client)
步骤1:拷贝服务端的AIDL文件到目录下(目的是生成一样的Binder类并实现相同接口)
步骤2:创建ServiceConnection(),并在Activity与Service建立关联时的回调方法中,使用Stub.asInterface接口获取服务器的Binder并将其转为AIDL接口,就可以调用对应的接口方法了
步骤3:通过Intent指定服务端的服务名称和所在包,bindService()方式启动来绑定远程Service(使用完后需要unBind());
3)其他的IPC方式
Bundle、文件共享、AIDL、Messenger、ContentProvider、Socket
性能优化
参考:https://www.jianshu.com/p/da63581a2212 https://blog.csdn.net/csdn_aiyang/article/details/74989318
优化思路可以从:稳定、流畅、消耗、安装包 (稳快省小)这四个方面依次着手;
具体有:内存优化、UI流畅优化(卡顿优化)、启动优化、电量/流量优化、apk瘦身
一、内存优化、崩溃异常(稳定)
●避免OOM:分析内存泄漏的现象,检查代码或通过MAT等工具分析
●避免Crash 和 ANR:做好Crash监控,把崩溃信息、异常信息收集记录起来
二、流畅优化
1)问题分析
●界面绘制任务过重(一帧耗时太长):主要原因是绘制的层级深(避免OverDraw)、页面复杂、刷新不合理;
●数据处理(阻塞UI线程):一般分为三种情况,一是数据在处理 UI 线程,二是数据处理占用 CPU 高,导致主线程拿不到时间片,三是内存增加导致 GC 频繁,从而引起卡顿。
2)优化建议
●布局优化:<include>布局复用、<viewStub>懒加载、<merge>合并重复布局、删除控件无用属性、减少自适应可缩短measure时间成本;
●绘制优化:避免OverDraw重叠绘制,移除非必需背景、移除Window 默认的背景、按需显示占位背景图片
3)启动优化
●UI 布局:优化闪屏页的 UI 布局,通过 Profile GPU Rendering 检测丢帧情况。
●启动加载逻辑优化:可以采用分布加载、异步加载、延期加载策略来提高应用启动速度。
●数据准备:数据初始化分析,加载数据可以考虑用线程初始化、缓存数据等策略。
4)刷新优化
●减少刷新次数、缩小刷新次数
三、电量、流量优化
●5.0 之后专门引入了一个获取设备上电量消耗信息的 API:Battery Historian 系统电量分析工具,通过工具分析应用耗电情况
四、apk瘦身
●使用代码混淆、删除冗余资源、图片资源压缩、插件化(功能模块按需从服务端下载)
内存泄漏
参考:https://www.jianshu.com/p/ab4a7e353076
长生命周期的对象持有短生命周期的对象的引用,导致短生命周期对象无法被 GC回收
一、引起原因
单例模式:持有当前Activity的引用(因单例的生命周期和应用一样长,所以应该传入application为context);
静态变量:静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束;
非静态内部类、匿名内部类:持有外部类的引用(如 Handler持有Activity,message又持有handler,so。。);
未取消注册或回调:持有类的引用(如广播);
集合中的对象:如果一个对象放入到ArrayList、HashMap等集合不需要使用的对象需要及时移除,否则对象无法被销毁;
资源未关闭或释放:如IO、File流或者Sqlite、Cursor等资源时要及时关闭,它们使用了缓冲对象,不关闭则无法被销毁;
属性动画:需要cancle()停止动画,否则虽然看不见但它依然在,且动画引用所在的控件又持有activity引用;
WebView:加载网页后会长期占用内存而不能被释放,需要将webview移除父容器再调用销毁;
二、内存泄漏的检测方法
1、MAT工具
2、LeakCanary工具
RecyclerView(加强版ListView、GridView)插件式的实现
参考:https://www.jianshu.com/p/f011fa974fbe
官方解释它为加强版ListView,但是它们不属于同一个组件,因此ListView也没有被废弃,应该根据场景来选择使用
ListView使用场景:简单的滑动组件,能轻松的使用divider,header,footer、点击事件这些功能,但ListView是紧耦合的;
RecyclerView场景:重点在复用、灵活,它可以深度定制化,低耦合的方式复用ViewHolder;(对于频繁更新,支持动画则使用RV比LV有更明显优势,它已经自动实现了ViewHolder类)
一、实现步骤(插件实现具体功能)
1)四大组成(需自己实现的“插件”,然后RV通过setXXX()添加):
Adapter:为Item提供数据的适配器。(需继承 RecyclerView.Adapter来实现)
Layout Manager:Item的布局,即控制显示方式(需继承RecyclerView.LayoutManager来实现,内置3种实现类)。
Item Animator:添加、删除Item动画。(需调用RV的setItemAnimator()设置动画)
Item Decoration:Item之间的Divider。(需调用RV的addItemDecoration(插件实例)来设置分割线)
2)实现点击、长按的方案
●通过mRecyclerView.addOnItemTouchListener去监听然后去判断手势,
●通过接口回调的方式,在adapter中定义回调接口,在activity中设置监听
二、用RecyclerView注意哪些方面,怎么用?
●三个注意点:RecyclerView.Adapter、LayoutManager、ItemAnimator
第一点:RecyclerView.Adapter
RecyclerView.Adapter中的viewholder它帮我们封装好了,不用像以前使用listview的适配器一样自己去写viewholder了。
第二点:LayoutManager
这个LayoutManager类决定视图的位置,它可以管理滚动和循环利用。它只有一个实现类:LinearLayoutManager,通过它设置横向和纵向。
第三点:ItemAnimator
ItemAnimator根据适配器上的通知去动画的显示组件的修改,添加和删除等。它还会自动添加和移除item的动画。
●优点:RecyclerView它只负责View的复用与回收,而不关心子View的具体布局、绘制或其他问题,跟数据展示相关的所有问题,都委派给了这些”插件化”的类来处理;它与Lv相比,它支持局部刷新,它的缓存机制是4级缓存而Lv是2级;可以轻松实现拖拽、侧滑删除效果
●缺点:RecyclerView需要自己实现点击事件、需要自己实现HeaderView 和 FooterView
ListView
参考:https://blog.csdn.net/s003603u/article/details/47261393
一、优化
1、convertView重用:convertView 是LV内置的缓存机制,用来复用 已缓存的View,切忌每次 getView() 都要新建;
2、ViewHolder优化:为解决findViewById方法耗时大,以节省去这个时间。通过setTag,getTag直接获取View;
3、图片加载优化:滑动时候不要异步加载图片, listView.setOnScrollListener()监听滑动;
4、onClickListener设置监听:在ViewHolder中设置一个position,然后viewHolder implements OnClickListenr,而不是getView()时设置新的onClick事件;
5、使用 RecycleView 代替,可进行局部刷新、View的复用和回收;
6、三级缓存、避免半透明元素、getView()中不要做耗时操作;
二、adapter
1、adapter将数据源和View视图进行了分离,也是一种MVC设计模式的实现
三、RecycleBin 缓存复用机制
1、该机制是对可见视图之外的头一个view 和 尾一个View进行缓存,以便复用,即convertView的复用,所以它加载成千上万条item也不会发生oom,这是ListView被设计出来时就已经考虑到的一种机制;
EventBus 事件总线
参考:https://blog.csdn.net/itachi85/article/details/52205464
基于发布与订阅的事件总线,简化各组件间的通信,开销小,代码更优雅,将发送者和接收者解耦;
它优雅的解决了组件间、Activity、Fragment间的数据传递;
一、重要点
1)EventBus的三要素(事件、订阅者、发布者)
●Event:事件,可以是任意类型的对象。
●Subscriber:事件订阅者(只能4种线程模型,3.0版本前使用时需指定方法名(方法名同时也是线程模型),3.0则用@Subscribe注解任何方法并指定线程模型)
●Publisher:事件发布者,可以在任意线程任意位置发送事件
2)四种ThreadMode(线程模型)
●POSTING(默认,哪里发出就在哪里处理,可阻塞并ANR)
●MAIN:(UI线程中处理,可阻塞、ANR)
●BACKGROUND:(UI线程发出则在新线程处理,否则哪里发则在哪处理,这模式禁止UI操作)
●ASYNC:都会在新建的子线程中处理,同样,此事件处理函数中禁止进行UI更新操作。
二、基本用法(详细见博客中介绍):
1.自定义一个事件类
2.在需要订阅事件的地方注册事件
3.发送事件
4.处理事件
5.取消事件订阅
三、粘性事件(发送后再订阅也能收到该事件,类似黏性广播)
注册粘性事件: @Subscribe(threadMode = ThreadMode.POSTING,sticky = true)
发送粘性事件: EventBus.getDefault().postSticky();
其他的步骤都一样
四、源码理解
EventBus是在一个单例模式,内部维持着一个map对象存储了一堆方法;在register注册时,遍历当前类将事件信息封装添加到一个List中,再以Class类型为key添加到Map中;发送post时,就根据参数key去查找那些方法,然后进行反射调用。核心架构是基于观察者模式来实现的。
Glide(图片加载框架)
参考:https://www.jianshu.com/p/7ce7b02988a4
https://www.cnblogs.com/whoislcj/p/5558168.html 总李写代码
创建Glide的主要目的有两个,一个是实现平滑的图片列表滚动效果,另一个是支持远程图片的获取、大小调整和展示(还可以等等功能)
Glide特点:1.Google 推荐,简单易用的API , 2. 专注平滑的滚动,3.高性能、可扩展,4.处理图和原图的缓存机制,5、显示示 Gif 和 Video, 6、请求优先级
一、使用方法(使用很简单,内在很复杂):
1)加载图片至少3步骤(with、load、into)
●Glide.with(context) .load(url) .into(imageView);即传入context并load它的Url,在view上显示;
2)过程就是:1、创建request 2、执行加载 3、回调刷新UI
二、源码理解:
1)with()创建request ,完成Glide的实例化
●创建RequestManager,它由RequestManagerRetriever的get()返回,它内部Lifecycle与RequestManagerFragment就是一个Fragment,它将Fragment的生命周期与RequestManager,
三、三大特点
●Glide缓存策略(支持内存缓存和磁盘缓存(即处理图缓存和原图缓存),默认Bitmap格式采用RGB_565,内存使用至少减少一半)
参考:https://blog.csdn.net/qq_37237245/article/details/72956376
Glide的缓存读取顺序是 内存–>磁盘–>网络,Glide默认是开启内存缓存和硬盘缓存的,可通过配置去禁用它,它分别缓存两种图片资源:原图和处理图,1、在内存中缓存处理图而不是原图,且根据ImagView大小动态加载或动态缓存(因此同一张图可能由于ImageView大小不同而多次下载并多次内存缓存)2、可配置为内存缓存处理图+磁盘缓存原图
●生命周期集成(根据Activity/Fragment生命周期自动管理请求)
●高效处理Bitmap(使用Bitmap Pool使Bitmap复用,主动调用recycle回收需要回收的Bitmap,减小系统回收压力)
四、对比
●Glide和Picasso :Glide可缓存处理图和原图,Picasso只缓存原图,Glide的请求基于生命周期,并且Glide更有利于减少oom的发生,GIF动画是Glide的杀手锏。而Picasso的图片质量更高(只显示原图)。
Rxjava(响应式编程库,基于观察者模式的事件流链式调用)
ReactiveX:Rx是 一个函数库,利用可观察序列和LINQ风格操作符来编写异步和基于事件的程序;RX=observebles (表示异步数据流)+LINQ(操作符)+Schedulers(调度器并发处理),响应式编程是面向数据流和变化传播的编程方式。
RxJava作用:异步处理、列表过滤、变换等等;它是在Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库;
四个主角:Observerbles、Observer、Subscriber、Subject。
参考 :http://gank.io/post/560e15be2dca930e00da1083 扔物线
一、使用3步骤:
使用参考:https://www.jianshu.com/p/a406b94f3188,注意Observer是接口类型,Observable 是抽象类型;
1、创建Observer<T>:观察者Observer或SubScriber,观察者至少重写3个方法:onCompleted()、onError()、onNext(T),定义对应事件回调处理;(onNext(T)为普通事件处理事件源T,onCompleted完结事件与onError异常事件是互斥的)
2、创建Observable<T> :create()创建被观察者,即返回一个Observable<T>,泛型中的T就是事件的类型,T会被发送到观察者。重写call() 方法可定义多个onNext(T)普通事件和一个onCompleted()完结事件,最终会将所有onNext()事件添加到一个任务队列中再依次发送T给观察者。// 此外还可以通过 just()、from()等操作符来创建Observable 事件源类型,详见操作符;
3、订阅subScribe:通过Observable.subScribe(Observer),即 被观察者 订阅 观察者(可多个),被观察者会将事件发生给观察者进行回调处理;
以上3步骤可以分开创建再进行订阅,也可以直接链式调用;
●Observable<T> 分为:Cold Observable 和 Hot Observable ,详见 https://www.jianshu.com/p/12fb42bcf9fd ;它有3种创建工厂模式 即:create(), from(),just();此外,一种Observable <T>只能接受一种事件类型,如果要发送多种类型的事件,应该用组合操作符,将多个Observable组合在一起再发送。
●Observer<T>:Observer是接口,SubScriber继承了它并扩展(多了onStart()和unsubscribe()),所以在subScribe订阅后会将Observer转换为SubScriber,它将接收被观察者发送的事件源T,并在onNext()中处理;
●观察者 Observer的subscribe()具备多个重载的方法,返回的 Disposable对象,通过Disposable.dispose() 可切断观察者与被观察者之间的连接;
二、不完整回调Action:
完整回调:观察者为 Observer或SubScriber,因为它定义了至少3个回调事件:onNext() 、onCompleted()、onError();
不完整回调:通过创建Action来作为观察者,取代了 Observer或SubScriber,这是一种可自定义且灵活度高的回调方式,例如:Action无返回值,或Action9(),这个数字代表了回调方法call(9种参数类型)的回调参数类型的数量;
注意:FuncX 和 ActionX 的用法类似,但 FuncX 包装的是有返回值的方法,而ActionX是无返回值的,都是用于自定义回调。
三、Subject(抽象类)一种特殊的存在
Subject继承了Observable,又实现了Observer接口,所以它可以代替Observable,并且作为一个桥梁连接Observer,所以Suject =Observer + Observable ,它即可以代替Observer 也可以代替Observable ,Subject有4种具体实现的子类。
●使用方法:详见 https://www.jianshu.com/p/240f1c8ebf9d 。注意文中提到的,当它作为Observable的时候直接代替、替换掉了Observable,但是它作为Observer或SubScriber,它只是一个桥梁,在 subscribe (Subject)时,又需要 Subject.subscribe( Observer),所以最终回调处理还是交给Observer来,Subject 只做为桥梁或代理者而存在,这个存在的意义是为何暂时不清楚~
●优缺点:详见 https://blog.csdn.net/PrototypeZ/article/details/51113828;优点:灵活性大,可动态在某时机添加事件并发送,缺点:错过真正关心的事件,非线程安全的,事件的发送是未知的,
四、操作符
分为:创建 、变换、过滤、组合、错误处理、辅助、条件、布尔、算术、聚合、连接操作符,操作符很多实际应用时需要查阅API,例如:创建Observable 时用到create()操作符,也可以用 just()和from()操作符,它们使用的规则不同,一般是在被观察者创建后,通过链式调用各种操作符来对事件做设置,接着才SubScribe订阅 观察者;操作符攻略 详见 https://www.jianshu.com/p/cd984dd5aae8
●变换与转换 操作符:变换操作符是将 “事件序列中的对象或整个序列进行加工处理,转换成新的事件或事件序列,并返回该对象”,加工处理可以是类型的转换,也可以是其他处理; 转换操作符是将“observable转换为另一种对象或数据结构”。
五、线程控制器(Scheduler)
如果不设置,默认是在调用subscribe() 的线程上进行回调处理,Scheduler内置5种线程模型,即:immediae()当前线程、newThread()新线程、 io() 无上限线程池,可复用空闲线程、computation()固定数量线程池且大小为cpu核心数,所以不要IO、trampoline()当前线程加入队列处理。
通过subscribeOn() 和 observeOn() 操作符来控制 事件产生 和 事件回调所在的线程,从以上5种模型进行选择。
注意:
●observeOn()可以多次切换线程,但subscribeOn()只能一次,多次subscribeOn()也等同于第一次所指定的。也就是事件产生线程只能指定一次,而事件回调线程可以多次指定、切换。
●Subscriber.onStart()与doOnSubscribe()区别:前者在事件发生之前不能确定subscribe() 所指定的线程,后者却可以在事件发生前就确定事件消费在指定的线程。
六、RxJava的使用场景
如:异步网络请求、有/无条件的轮询、取代多层次的嵌套回调、失败重连、合并数据源并展示、获取缓存数据、功能防抖、背压策略、线程操控等。。。(都是依靠它丰富的操作符来实现的)
参考链接 :https://www.jianshu.com/p/2c54f9ccd52f https://www.jianshu.com/p/2caa581425a3
Retrofit + RxJava:把Retrofit 的请求封装进 Observable ,在请求结束后调用 onNext() 或在请求失败后调用 onError()
七、常用操作符
Rxjava的一些常用操作符如:create 、just 、 from 、map、flatMap、concatMap vs flatMap
●创建操作符:基本创建create ,快速创建just、fromArray等、延时创建defer、range等。
●变换操作符:
Map():将被观察者发送的事件转换为任意的类型事件,如将事件Integer 类型变成了 String 类型;
FlatMap()无序:将被观察者发送的事件序列进行 拆分和 单独转换,再合并成一个新的事件序列(合并为一个新的observable),最后再进行发送;
ConcatMap()有序:类似FlatMap()操作符,但它保证了拆分和重组后事件序列的顺序;
Buffer():定期从事件序列中 获取一定数量的事件并放到缓存区中,最终发送;
●组合操作符:组合多个被观察者一起发送数据,合并后 按发送顺序串行执行
concat() / concatArray():concat组合被观察者数量≤4个,而concatArray则可>4个,
merge() / mergeArray():也是组合,与concat区别:它是并行执行,而concat是串行
OKHTTP(基于建造者模式、责任链模式)
使用了JDK中HttpUrlconnection进行网络请求,内部封装线程池。
一、使用过程(3个主角)
1)过程介绍:创建Okhttplient和Request实体,由Okhttplient 调用newCall(Request)得到Call对象,再由Call 调用enqueue() 异步(或execute()同步)发送请求,在异步时需传入形参CallBack来对response做回调处理;
●OkhttpClient:发送请求的客户端,以单例存在以便获取共同的response缓存和线程池,它是发送网络请求的主体;
●Request:对请求体和参数进行封装,且可以设置HTTP请求头等,最后由build() 构建出它的实体(建造者模式),当上传文件时需构建requesBody并传给request;
●Call:它是连接request和response的桥梁。通过OkhttpClient.newCall(Request)返回Call对象,再由Call调用execute()或enqueue(CallBack)完成同步或异步的请求,最终得到response,异步时又由Callback进行回调处理(CallBack是在工作线程中执行的);
总结:OkhttpClient 根据Request得到一个Call,再由Call去调用同步或异步方法完成请求,得到response。
2)其他功能
●上传文件:构建RequesBody将file传入,最后由Reques进行post;
●缓存策略:OkhttpClient设置缓存路径,Reques.cacheControl()来配置缓存实体
3)建造者模式(开源框架的构建大多采用的方式)
●builder():将一个复杂对象的构建和它的表示分离,即:同样的构建过程可以有不同的表示;
●2.0与3.0版本区别:2.0版本可以直接new 出okhttpClent对象,而3.0版本需通过build()返回实体对象,通过OkhttpClient.Build().具体的链式编程函数来完成 超时和缓存等设置;
二、源码理解
1、RealCall:(newCall()返回的实体对象是RealCall,然后调用它的异步/同步执行方法)
2、Dispatcher 任务分发器(线程池+反向代理):解决多线程并发请求和复用问题,但在同步请求时也仍然使用了它,并且在请求结束后,拦截器会通知Dispatcher进行相应的资源关闭和回收;在异步请求中,则使用到线程池;
3、Interceptor拦截器(责任链模式):用来添加、移除、转换请求和响应头,以及各种强大功能;
4、缓存策略(未总结)
5、失败重连
源码总结:
●通过builder()来设置Request,这种基于建造者模式的构建方式使得同样的构建过程可以有不同的表示;●OkHttpClient将Request转换为一个RealCall,而RealCall再将请求交给Dispatcher任务调度器进行同步或者异步的发送;●Dispatcher内部由线程池+反向代理模式,以此来解决非阻塞和高并发的问题;●最后,需要通过拦截器interceptor发出网络请求和处理返回结果。●拦截器用了责任链设计模式,请求一层层外下传直到直到得到response再一层层往上回传。
三、Interceptor拦截器(精髓所在)
●它不仅仅是拦截一些额外的处理(如修改请求头),实际上它把网络请求、缓存、透明压缩、重定向等功能都统一起来了,每一个功能都被封装成一个Interceptor拦截器,把所有的拦截器放入到List集合中,最后用Interceptor.Chain 将他们形成一条链;
●Interceptor 的设计使用的是分层的思想,每一个Interceptor就是一层,类似iso的分层,每一层都有自己的单一职责,这样便于拓展
●同步或者异步请求最终都是通过Interceptor拦截器进行发送的
Retrofit (基于OKHTTP、RESTful)
参考:https://www.jianshu.com/p/a3e162261ab6
它是一个基于Okhttp,通过注解来配置网络请求参数、支持同步、异步请求 的网络请求框架,网络请求的工作本质上是 OkHttp 完成,而 Retrofit 仅负责网络请求接口的封装;
一、基本原理
通过 Retrofit 请求网络,实际上是使用 Retrofit 接口层封装请求参数、Header、Url 等信息,之后由 OkHttp 完成后续的网络请求操作。在服务端返回数据之后,OkHttp 将原始的结果交给 Retrofit,Retrofit再根据用户的需求对结果进行解析。
1)使用过程(具体使用过程参见博客):
●定义请求接口(使用注解对请求参数进行包装,接口方法返回的是Call对象);
●创建retrofit实例(通过builder()来构建该Client);
●创建请求接口实例(通过retrofit.create()来创建接口,再由接口getXXX()得到Call对象,其目的就是将retrofit接口转换为Call);
●由Call 调用异步enqueue() 或 同步execute() 发起请求,在异步时传入CallBack进行回调处理(这里就是Okttp中的请求过程);
总结:由此可见,retrofit先对请求参数做了封装,最终转换为Call,由Call发起okhttp的网络请求并回调处理。
2)Retrofit Client
●URL的组成:Retrofit 把网络请求的URL 分成了两部分设置,创建retrofit实例时定义的baseUrl 和各个请求接口时封装的部分;
●数据解析器:Retrofit支持多种数据解析方式,默认是CallAdapter,也就是通过Call发起的请求
3)常用注解(3种类型,详见博客)
●网络请求方法:如@get、@post对应与Http协议中的参数设置,@http则包含了所有的设置是一个个总和,通过method、path、hasBody来赋值设置,{id}是一种变量的赋值方法参见博客;(该类型注解都是在接口名的上一行使用)
●标记类:用于说明请求体的数据格式,如表单、字节流;(也写在接口名上面)
●网络请求参数(详见博客):
@Header & @Headers:添加请求头 (作用于参数)&添加不固定的请求头(作用于方法);
@Body:以 Post方式 传递 自定义数据类型 给服务器
@Field & @FieldMap:发送 Post请求 时提交请求的表单字段,与 @FormUrlEncoded 注解配合使用
@Part & @PartMap:发送 Post请求 时提交请求的表单字段,与 @Multipart 注解配合使用(比@Field功能更多可提交数据流)
二、源码理解
1)动态代理
●retrofit.create()时就通过动态代理模式,把我们定义的接口转换为接口实例
三、结合RxJava的使用
参见:https://www.jianshu.com/p/2c54f9ccd52f
RxJava是基于观察者模式的异步处理,传统的异步处理需要传入CallBack,而Rxjava是由observer来处理回调事件。所以retofit用于包装网络请求参数,rxjava负责异步回调。
需引入适配器的依赖:compile'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
1)注意区别:
●请求接口返回值:正常的retofit 的请求接口返回的call对象,与RxJava结合使用,则返回Observable。
●请求的发送:正常的retofit 是通过实例化请求接口得到Call对象,调用enqueue(CallBack)进行异步,与RxJava结合时,因为接口中定义了返回值为Observable,所以在实例化接口后得到的是Observable对象,再根据Rxjava的使用规则:Observable.subscribe(observer)完成异步。
●其他步骤:与 retofit 请求过程都一样
总结:Observable取代了Call,将retofit包装的请求参数封装到Observable中。
Butterknife
参考:https://www.cnblogs.com/whoislcj/p/5620128.html
https://blog.csdn.net/donkor_/article/details/77879630
ButterKnife是一个专注于Android系统的View、Resource、Action的注入框架(依托JAVA注解机制来实现辅助代码生成的框架),在编译成java字节码时就会对注解进行解析,而非反射的加载。
通过注解的方式简化代码,从而不必自己写findViewById和setOnClickListener代码;
一、基本原理
1)功能
●以绑定一个View
●给一个View添加点击事件
●给多个View添加点击事件
●给ListView setItemClickListner
2)注解原理
在编译阶段执行的,读入Java源代码解析注解,然后生成新的java代码,新生成的java代码最后被编译成字节码
●开始它会扫描java代码中所有的ButterKnife注解
●通过ButterKnifeProcessor 生成新的java类,即<className>$$ViewBinder 类
●调用bind()加载生成的ViewBinder类,此时就动态注入了所有View的属性
二、使用步骤
1)Zelezny插件快速生成
2)手写(引入依赖后)
●寻找id :通过注解@BindView、@BindDrawable、 @BindArray、 @BindColor 、@BindView等等
●设置监听:通过注解 @OnClick
●action修改:通过ButterKnife.apply()
●最后必须调用 ButterKnife.bind(this)进行绑定
二、注意事项:
●在Activity中使用,ButterKnife.bind(this) 必须在setContentView()之后,且父类bind绑定后,子类不需要再bind;
●Fragment 或 自定义View中使用与Activity相同,但ButterKnife.bind(this, contentView)要传入contentView,且需要解绑 Unbinder.unbind(),在Activity中不需要做解绑操作;
●在异步请求后更新UI,需要判断 Unbinder==null,否则解绑后再去更新UI就报错;
●属性布局不能用private or static 修饰,否则会报错 (因为使用private就只能通过反射来获取了)
●setContentView()不能通过注解实现。
●ButterKnife版本7.0.1以上是@Bind~~,以前的是@InjectView了。
●ButterKnife不能在你的library module中使用,这是因为library中的R字段的id值不是final类型的,但是自己的应用module中确是final类型的。
视频播放、流媒体
参考:https://blog.csdn.net/wozuihaole/article/details/60867076
一、实现视频三种方式:
1.MediaPlayer+SurfaceView;(个性化定制强)
2.VideoView;(属于系统自带控件)
3.Vitamio框架。(支持多做视频格式、硬解码、GPU渲染,API简洁、且支持流媒体格式)
二、流媒体协议有:RTP、RTCP、RTSP、RTMP、MMS、HLS、HTTP(各大直播平台在用:rtmp&hls)
三、流媒体框架:ffmpeg
事件分发、点击冲突
参考:https://www.jianshu.com/p/38015afcdb58
事件分发思想(责任链模式):上级可以拦截并处理事件,或向下级分发事件,直到有人拦截并处理,否则又将事件逐层返回给上级,最终交给顶层进行消费处理,这是一种责任链设计模式;
一、基本概念
1)Touch点击事件:被封装成MotionEvent对象,其中有4种事件类型:ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANCEL(非人为原因),因此,这个MotionEvent事件传递的过程 = 分发过程。
2)传递顺序:Activity -> ViewGroup -> View
(Activity创建时候会创建一个window(具体子类为phoneWindow),它内部类为顶级的DecorView,DecorView又继承自FrameLayout(一个ViewGroup),所以,Window其实也就是ViewGroup)
3)3个方法:dispatchTouchEvent() 分发、onInterceptTouchEvent() 拦截 ,和onTouchEvent() 处理;
(后面两个方法都是在第一个方法中的内部调用,而 onInterceptTouchEvent() 仅在ViewGroup中调用,当对事件进行了拦截即返回了ture,则会调用onTouchEvent() 进行处理,并返回true通知上层事件已被处理,否则事件仍然会回传交给上层处理)
4)事件优先级:onTouch()-> onTouchEvent()-> onClick()
二、注意
1、如果底层的View不拦截,则事件序列的后续事件不会再分发给这个View。而ViewGroup和Activity却不同,仍然可以接收后续事件;
2、View如果拦截了某事件序列,则该事件序列都交给它处理(拦截了事件,就会调用onTouchEvent()对该事件处理,返回ture则不再向上传递事件,false是交给上级处理,即:虽然拦截了也处理了,但是还是传递给上级再处理),
3、ViewGroup拦截了Down事件,则事件不再向下分发,他的onTouchEvent返回ture表示处理该事件,则Down事件不会向上传递;后续事件也交给它处理,但它的onInterceptTouchEvent() 只调用一次,后续的事件将不再调用;
4、ViewGroup需收到重写onInterceptTouchEvent()返回ture进行拦截
三、点击冲突
1、外部拦截:由父容器ViewGroup做决定是否拦截,重写父容器的onInterceptTouchEvent()方法,在内部做相应的拦截即可。
2、内部拦截:都给子View拦截,但子View决定是否处理事件,否则就交给父容器进行处理。内部拦截法需要配合 getParent().requestDisallowInterceptTouchEvent()方法,让子View控制是否允许父布局拦截事件。
自定义View
参考:https://www.jianshu.com/p/146e5cec4863
一、基本定义
1、View视图是树形结构,无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即 树形递归),最终计算整个View树中各个View,最终确定整个View树的相关属性。
2、自定义View三个过程:measure、layout、draw(都是final类型,只能重写它们对应的onXXXX()方法);
二、具体过程
1、measure:测量view的宽高(但不是最准确的,只有在onLayout()中获取的宽高才是最终的);
1)两个重要参数
●ViewGroup.LayoutParams(布局参数,它不包含padding值):指定View 的高度(height) 和 宽度(width)等布局参数;
●MeasureSpec(测量规格):MeasureSpec(32位int值) = 测量模式(mode,高2位) + 测量大小(size,低30位);
●测量模式有3种:未指定的/不限制的、精确的、至多的;
2)MeasureSpec的获取:子View的MeasureSpec是根据 自身的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,而顶层的 DecorView的父容器的MeasureSpec值= 窗口尺寸;
3)测量过程:区分子View 和 ViewGroup 测量
●子View:measure()--> onMeasure() --> setMeasuredDimension(getDefaultSize(计算spec) ) --> 完成测量
在onMeasure()调用了setMeasuredDimension(),测量就是在这里完成的!这个方法是重中之重,而getDefaultSize()则是去计算宽、高的spec值,最终返回宽、高spec两个参数给setMeasuredDimension()调用;
可理解为先计算后存储:getDefaultSize() = 计算View的宽/高值,setMeasuredDimension() = 存储测量后的View宽 / 高,而子类只能去重写onMeasure();
●ViewGroup:遍历测量所有子View的尺寸,将所有子View的尺寸进行合并;因为不同的ViewGroup的具体实现规则不一样无法统一,所以必须复写onMeasure()来具体实现布局规则;
4)测量过程总结:ViewGroup会去遍历它所有子View 的measure(),根据ViewGroup的MeasureSpec和子View自身的LayoutParams来决定子View的MeasureSpec测量规格,根据这个测量规格来进一步获得子View的测量宽、高,然后一层层向下传递,并保存父控件的测量宽、高,整个调用过程就是一个树形的递归。
2、layout:确定View的四个顶点位置(相对于父View的位置)
1)过程:layout() --> onLayout() --> setFrame()--> 完成
●单一View:layout() 中调用setFrame() 计算View自身的位置,最后调用 onLayout() 是一个空实现,由子类具体去具体实现;
●ViewGroup:计算自身ViewGroup的位置:layout(),再遍历子View & 确定自身子View在ViewGroup的位置(调用子View 的 layout()):onLayout();
2)布局过程总结:与Measure过程类似,都是对树形结构的遍历,ViewGroup计算自身layout() 的位置,然后遍历子View的layout(),子View要在onLayout()中具体实现自己的布局规则,来确定自己相对于父View的位置;
3、Draw 绘制过程:
1)绘制过程:draw() 自身 --> drawBackgroud() 自身背景 --> onDraw() 自身内容 --> 装饰(渐变框,滑动条等等)
2)区别于:单一View仅绘制自身,ViewGroup则绘制自身内容后,通过dispatchDraw() 再遍历绘制子View,整体的绘制流程相同
●绘制自身内容:必须复写onDraw()来完成;
●dispatchDraw():ViewGroup通过该方法去绘制子View,子View再逐一绘制,而子View中的这个方法是一个空实现,也不需要复写;
3)注意:刷新方法
● invalidate():当尺寸未发生变化时,则不会调用layout();
●requestLayout():当尺寸发生变化时候,会触发调用 measure() 和 layout(),但不会调用Draw();
三、具体使用
自定义View的的案例:绘制一个空心的扇形图(继承View或ImageView)、给字体添加特殊背景(TextView)、绘制圆角图片(继承ImageView)、不规则View等等。View是界面层控件的一种抽象,它也是java的一种对象,所以可以在代码中使用它,安卓基于MVC模式,所以我们在xml中写的标签,就是用了安卓内置的自定义属性,在xml中配置了View的属性值(标签就像key-value 一样),在编译的时候,xml解析器就会将标签中key对应的value值赋值给View对象,这个视图对象在经过测量、布局、在绘制时通过2D绘图来做运算处理,调用硬件做渲染,然后以十六进制的颜色值传递给屏幕,屏幕在各个像素点上显示出来,再不断的刷新。。。
1)自定义View
●继承View:继承顶层的View(测量和布局在View内部都已经实现好了),只需要 重写onDraw(),并且要处理wrap_content 和padding。即对自己要实现的View做具体的布局和绘制,一般是在自带组件不能满足需求时才会这样做。
●继承已有的View:如继承TextView(安卓自带的View组件已经为我们封装好了),则不需要处理它的wrap_content和padding,只需要对它进行拓展的部分进行重写,如给TextView绘制背景颜色,只需要重写onDraw(),调用Canvans和Pait的API即可实现。
1)自定义ViewGroup
●继承ViewGroup:因为ViewGroup内部的测量和布局都是空实现,需重写onMesure()和onLayout(),因为不同的容器测量和布局规则不一样只能由自己去写它的逻辑代码,又因为它是一个容器,所以还要处理子元素的测量和布局。
●继承已有ViewGroup:在已实现的容器基础上进行拓展,根据需要拓展的部分去重写。如给ViewGroup加一个边框,加一个底部分割线,这些都属于增加新的视图,而不需要去修改它的测量和布局,那么只需要重写onDraw() 调用paint和canvans即可。
总结:大多数情况的自定义View只需要重写onDraw(),在这里面调用2D绘图的各种API去对视图进行绘制。完成自定义View类后,就可以在xml中去使用它,或者动态addView()将一个View添加。而自定义ViewGroup难度比较大,如果要自己实现一个目前没有的容器是比较麻烦的,可以总LinearLayout的源码中看出,它的测量和布局过程都比较复杂,对横向竖向以及 滑动时的状态都做了比较多的逻辑判断,所以我们一般是在已有的容器上做拓展。
3)wrap_content 和 padding处理
4)自定义不规则View
参考:https://www.jianshu.com/p/f4088146f5ad
自定义属性 (attr标签)
参考:https://www.cnblogs.com/wjtaigwh/p/6594680.html
我们在XML中使用的是系统已经定义过的属性,如background、padding、id等,我们也可以自己去定义属性名和属性值,让控制在XML中使用它,最终会由java的xml解析器将其解析,并封装成对应的java类,从而去控制一个View的实际情况。
一、步骤
1、在attrs文件中创建
2、标签层级:最外层<resources>,然后<declare-styleable name="属性控件名">
●最后在里层可以定义多个 <attr name="" format="">标签
●如果带format就是限定传入类型(有11种),可以不带format,则在name引用系统已有的android:XXX
二、使用:与系统定义的属性用法一样
LayoutInflater.inflate 布局加载器
参考:https://blog.csdn.net/yingpaixiaochuan/article/details/52966805
一、关于LayoutInflater类inflate(int resource, ViewGroup root, boolean attachToRoot)方法三个参数的含义:
●root:需要附加到resource资源文件的根控件:就是inflate()会返回一个View对象,如果第三个参数attachToRoot为true,就将这个root作为根对象返回,否则仅仅将这个root对象的LayoutParams属性附加到resource对象的根布局对象上,也就是布局文件resource的最外层的View上,比如是一个LinearLayout或者其它的Layout对象。
●attachToRoot:是否将root附加到布局文件的根视图上
总结:两个参数和三个参数的区别
动画
动画分有两种:View动画和属性动画,View动画只左右在“视图”而不是“对象”上,所以它不可以改变其他的属性,如颜色、背景长度等;而属性动画则是通过改变对象的属性来完成动画效果。
一、Animation —— View动画(有4种补间动画、帧动画、组合动画)
参考:https://www.jianshu.com/p/733532041f46
4种补间动画分别为:平移、缩放、旋转、透明;还可以通过组合的方式将几种补间动画组合同时使用;帧动画就是将多张视图的集合进行逐帧播放;
1、使用方式有两种,XML设置 和 代码设置;
1)XML设置
●在res/anim 文件夹创建xml文件,使用需要的补间动画标签,如<translate >,并根据它所定义的属性赋值;
●在java类中,通过AnimationUtils.loadAnimation()加载该xml文件,它解析后赋值给Animation对象,最终通过view.startAnimation() 启动这个Animation对象。
2)代码设置
●直接new 出需要的补间动画对象,如new TranslateAnimation()会返回Animation 对象,再去设置该对象的各种值
●最终也是 view.startAnimation(Animation )启动该动画对象
总结:两种方式的启动方式都一样,区别于构建Animation的过程,xml定义属性是由AnimationUtils解析器将它解析后赋值给java对象,等同于在java代码直接设置对象的属性值。
2、组合动画
使用< Set> 包裹其他4种补间动画,来完成组合,它相当于一个集合容器将多个包装起来,它也是可以在xml中和代码中两种方式去设置
●xml中设置就是用<set>标签包裹其他标签,使用方式和单个的使用相同;
●代码中:通过new AnimationSet(true)得到AnimationSet 组合对象,调用它API进行设置相关设置,最后addAnimation()各个单一动画实例,将它们加入这个set容器中,并可以after()、with()、before()等设置执行顺序,启动方式与单个的相同;
3、监听动画
●Animation.addListener()可传入监听者,对动画的执行过程进行监听并回调处理,在java中setAnimationListener()传入监听者即可实现外部回调,这种方式需要重写4种方法
●如果传入的是AnimatorListenerAdapter(动画适配器接口)监听者,就可以重写某一个回调方法;
4、应用场景(Activity 切换效果、Fragement 切换效果、ViewGroup中子元素的出场效果)
●在startActivity(intent)后,通过overridePendingTransition()传入进入和退出的xml动画,也可以选择系统自带的 android.R.anim.XXX效果
二、Animator——属性动画
参考:https://www.jianshu.com/p/2412d00a0ce4
属性动画是对view对象或任何对象的进行操作(不局限于view对象),功能更全,解决了补间动画的缺陷;两个重要的类 ValueAnimator 类 和 ObjectAnimator 类 ;使用方式都有两种,即XML设置和代码设置。
1、ValueAnimator (对值操作,手动刷新)
●XML中:使用<animator >标签,需要指定valueType和interpolator估值器,完成有关属性设置后,在代码中使用AnimatorInflater.loadAnimator()去加载该xml文件并返回Animator对象,对象调用setTarget(view)注入需要执行动画的view,再start()启动。
●代码中:ValueAnimator 有3个建造方法 ofInt()、ofFloat()、ofObject() ,来构造出3种不同类型的ValueAnimator 对象,然后再调用对象相关的API去进行动画有关设置,最后start()执行动画,另外必须要 ValueAnimator.addUpdateListener() 添加AnimatorUpdateListener监听者,在监听的回调方法中会返回新的ValueAnimator对象,每一次都要从新对象中取出对应的值,并赋值给需要的View对象, View再requestLayout()进行重绘,整个过程就是对View的某一个属性值进行不断的改变再重绘,达到动画效果。
因此,ofInt()、ofFloat()使用方式都一样,区别于传入的参数是 int或者float,因为内部使用不同系统默认估值器:IntEvaluator和 FloatEvaluator,而 ofObject() 接收的是一个对象,所以要自定义估值器,继承TypeEvaluator并复写evaluate(),完成对象中相关值的变化计算,最终返回新的object对象,在使用ofObject() 构造ValueAnimator对象时传入自定义的估值器,也是需要对ValueAnimator设置监听者,在监听回调方法中,得到新的对象并取出对应的值,对View进行设置后重绘,所以他们的使用步骤都一样,关键点就是在监听者中,每次变化都会调用该回调方法;
2、ObjectAnimator(对值操作,自动刷新)
ObjectAnimator继承自ValueAnimator,所以它的原理离不开ValueAnimator,并进行了拓展,也是通过3种建造方法构造出ObjectAnimator对象,在xml中使用<objectAnimator >标签,然后加载方式与上面的一样,Java代码中使用也是一样的,需要注意的是它的构造法
●第一个参数:传入要执行动画的view;第二个参数:指定要执行动画的效果(如4种补间动画)并在之后的参数指定执行过程的数值
●第二个参数还可以参入任何属性,第二个参数作用是:让ObjectAnimator类根据传入的属性名 去寻找 该对象对应属性名的 set() & get()方法,从而进行对象属性值的赋值;也就是它会根据这个名“XXX”去对象的方法中寻找setXXX()和getXXX()方法,如果对象的属性中没有get或set方法则报错或无效,我们可以通过1、继承该view,添加相关的get和set方法设置属性值 2、通过包装类(装饰者模式)提供set和get方法去间接的设置该对象的值;
●第三个参数:传入自定义估值器(如是是默认的int或Float类型则不用传)
三、AnimatorListenerAdapter 动画适配器 与 AnimatorSet 组合
addListener添加动画监听者,可传入AnimatorListenerAdapter适配器,则不需要重写4个方法,只重写自己需要回调的方法即可;AnimatorSet 是一个集合,它可以组合使用各个动画;
四、ViewPropertyAnimator
一个由谷歌封装好的动画框架,View.animate().xxx().xxx();一系列链式调用即可完成,并且自动执行,因为内部实现了start()方法,
五、插值器 & 估值器
参考:https://www.jianshu.com/p/2f19fe1e3ca1
估值器(TypeEvaluator)决定具体变化的数值,插值器(Interpolator)则决定值的变化模式(如加速、匀速、先加速后匀速等),都有两种使用方式:xml和代码设置
1、插值器:安卓内置9种插值器,xml中使用 interpolator属性来设置资源ID,即android:interpolator="@android:anim/overshoot_interpolator",也可以通过代码直接new出对应类型的插值器,如new OvershootInterpolator(),然后Animation再setInterpolator()该Interpolator,因其是个接口,所以可以自定义插值器,复写的方法中关键点在于:对input值 根据动画的进度(0%-100%)通过逻辑计算 计算出当前属性值改变的百分比,可以参见已有插值器的写法
2、估值器:系统内置了3种估值器,它的作用是设置属性值 从初始值过渡到结束值 的变化,是具体数值的反映,使用方式如上介绍,也可以自定义在重写的方法中去实现自己的规则。
WebView
一、注意事项
●WebView.addJavascriptInterface()没有被限制时,攻击者可以通过反射机制利用该漏洞任意执行java对象的方法
●webview写在其他容器中,可能发生内存泄漏
●jsbridge:Native与H5交互的桥梁
●webviewClent.onPageFinished() 在一个网页加载完成时会被调用,某些情况它会被多次调用,所以是一个坑;
●后台耗电:webView会自动开启一些线程去完成异步任务,且它会一直在后台运行非常耗电,可通过虚拟机暴力清除;
●webView开启硬件加速时,导致页面渲染问题(页面白块或者闪烁),只能关闭硬件加速
二、内存泄漏
●WebView内部执行的操作是会在子线程中,所以它持有Activity的引用就相当于内部类持有外部类的引用,解决办法:将WebView运行在一个独立进程,与APP进程隔离开来,在关闭WebView的时候直接干掉进程;
●动态添加WebView,在布局中创建一个ViewGroup用来放置WebView,Activity创建时add进来,在Activity停止时remove掉,且传入webView中的context使用弱引用;
APP项目结构设计
1、根据包名
组件化
组件化开发就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一成一个apk,这就是组件化开发。
一、组件化重点
●分模块
●路由机制
插件化(由宿主APP去加载以及运行插件APP)
参考:https://www.jianshu.com/p/b6d0586aab9f
插件化开发和组件化开发略有不用,插件化开发时将整个app拆分成很多模块,这些模块包括一个宿主和多个插件APP,每个模块都是一个apk(组件化的每个模块是个lib),最终打包的时候将宿主apk和插件apk分开或者联合打包。
插件优点:解决方法数 65536爆棚,宿主和插件分开编译,并发开发,动态更新插件,按需下载模块(但无法上架Google Store)
一、插件化需要解决的问题
●动态加载apk(用到:类加载器、反射和动态代理,过程是:宿主app通过一个代理的Activity,由类加载器去sd卡中加载一个未安装的apk,再通过反射获取类中的成员变量和方法,获取到“设置代理对象”方法和“onCreate()”方法)
●资源加载(通过AssetManager类和反射,来获取未安装的apk中的资源的方法)
●代码加载(通过反射来获取Acivity中所有的生命周期方法,再与代理Activity中的生命周期进行同步)
1)类加载器:将java字节码添加到JVM虚拟机中,在android中有两个类加载器
●DexClassLoader:从dex实体中的jar文件加载字节码的类加载器,主要用于动态加载apk、热更新
●pathClassLoader:加载文件目录下的apk
二、插件化原理总结:
1、通过DexClassLoader加载。
2、代理模式添加生命周期。
3、Hook思想跳过清单验证。
热更新
参考:https://www.jianshu.com/p/d17519d4952e
app崩溃率应控制在千分之一以下;热修复的原理大致就是:通过dex分包方案,将新的dex包放置在dexElements数组的最前面,根据Java ClassLoader机制,ClassLoader会去遍历这个数组,并优先加载新的dex文件而不再加载有问题的那个dex文件,这就完成了热修复。
一、热更新大致流程
1、线上检测到严重的crash
2、拉出bugfix分支并在分支上修复问题
3、jenkins自动化构建和将补丁生成
4、app推送或主动拉取补丁文件
5、将bugfix代码合到master上
二、热更新框架
1、Dexposed(出自阿里,源自Xposed框架,基于aop思想,功能包括有日志记录、性能统计、安全控制、修复处理,通过hook的方式来实现)
2、AndFix(出自阿里,单纯的一个热更新框架,而不是通过反射的形式)
3、Nuwa(基于类加载器ClassLoder 和dex分包)
三、热更新原理
1)Android类加机制(有两种类加载器)
●DexClassLoader:从dex实体中的jar文件加载字节码的类加载器,主要用于动态加载apk、热更新(能够加载未安装的dex,但是这个dex文件一定要在使用者的App目录中)
●pathClassLoader:加载文件目录下的apk(只能加载已经安装的dex文件)
2)热修复机制原理:
●dexElements数组:放置dex文件对象
●ClassLoader会去遍历这个数组:线上定位到有Crash的dex文件,修复好了以后将新的dex文件放在dexElements数组的最前面,ClassLoader会去遍历这个数组,并优先加载新的dex文件而不再加载有问题的那个dex文件。
进程保活
一、进程优先级(前三个一般不会被回收,后两个很容易被回收)
前台进程 > 可见进程 > 服务进程 > 后台进程 > 空进程(用于做缓存,便于下一个新进程复用)
二、进程回收测量
Lowmemory killer机制:通过一些比较复杂的评分机制,对进程进行打分(OOM_ODJ 打分机制),然后将分数高的进程判定为bad进程,杀死并释放内存
三、进程保活
1、利用系统广播拉活(缺点:可被手机管家禁止,且无法保证一被杀死就立即拉活,因为系统广播是不可控的)
2、利用系统Service机制拉活(因为service在内存不足时被干掉,系统在内存足够时去尝试重新启动)
3、利用Native进程拉活(5.0后被严格禁止了)
4、利用JobScheduler机制拉活
5、利用帐号同步机制拉活
组件化
组件化开发就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一成一个apk,这就是组件化开发。
MVP架构
参考:https://www.jianshu.com/p/5c3bc32afa36 吴七禁的乞丐版到时尚版
MVP的作用就是将V层与M层隔离,使得V层的职责更加单一,数据的处理只由M层负责,但是初级的MVP代码很冗余,每次都要写P层,所以进行高级的演变,对P层进行复用,以及用基类减少代码量。
一、MVP一般用法
View层只负责处理 UI;P层为业务处理且为纯Java类,不涉及任何Android API,它持有V层和M层的引用;M层负责数据处理,数据源;三层间调用顺序为view->presenter->model,M层通过Callback接口返回数据给P,P通过View接口返回给V层,所以接口层级传递的重要参与者。
1、M层:定义CallBack的接口方法,如成功、失败、错误、完成等;M层的构造法中需要传入CallBack的实现者,M层得到callback的引用,在对数据处理的不同情况就通过引用来调用callback中定义的方法,这是接口回调的用法。
2、P层:定义View层接口方法,如成功或失败时显示、隐藏某些UI组件等等,P层实现了CallBack接口,在构造法中new 出M的实例,并以自己为形参传入;此外P层构造法还接收一个实现View接口的实例引用,这样P层就完成了M层和V层的引用获取。
3、V层:实现View接口中的方法,并完成对应方法的UI操控,最后还需要new 出P层实例,将自己为形参传入P层构造法。
注意:P层持有V和M层的引用,V层可能存在内存泄漏的情况,每个V层对应一个P层,代码量会很大,每个M层负责不同的数据处理所以M层是独立可复用的,如果P层也实现单一职责,那么V层可以通过实现多个View接口和多个P层实例来组合使用,但是这种设计不严谨架构不健壮。
二、MVP的高级用法
1、公共接口:定义M层公共接口CallBack<T>来接收任意的数据实体,以及定义基础的接口方法;定义V层公共接口 IBaseView,并定义基础的控制UI方法和 getContext()方法;
2、基类P层:定义抽象类 BasePresenter<V extends IBaseView>,使用泛型来约束成员变量中V必须为IBaseView的子类,基类P层增加与V层同步的生命周期方法,P层在自己的生命周期中去动态绑定或者解绑P层等,如与V层的绑定不在构造法中而是在P层的创建周期中,P层还应提供getView()返回View实例;
3、基类V层:定义抽象类 BaseActivity或者BaseFragment 通过泛型来约束V和P的实例必须继承自以上基类接口,V的生命周期方法中去调用P层已定义好的生命周期方法,这样实现了V层与P层的周期同步,V层提供getPresenter()方法获得P层实例
4、M层优化:定义抽象基类 BaseModel<T>让所有具体M层去继承,并提供必须的abstract方法和不必须的普通方法;另外,P层不再负责创建出具体M层,而是通过DataModel类作为路由,由P层传入一个token值(包名+M层的类名),让DataModel根据token去创建具体的M层,DataModel再用反射机制去创建具体M层。
三、更高级的优化
https://blog.csdn.net/yang542397/article/details/78074629 ,文中主要优化V层与P层的解耦
1、文中第四提出了MVPVM:也就是V层与中间层VM绑定,VM再与P层绑定,P只需要发送数据给VM,VM收到后就立即调用同步V的更新方法,但是VM也一个bean对象,它对应了V层中所要展示的数据实体,但是扩展性不好,VM层无法让V层进行与数据实体无关的视图操作。
2、V层动态代理+缓存:V层通过动态代理,不再与P层的直接绑定,而是由V层的动态代理对象(View视图代理)与P层进行绑定,P层不需要改变,它持有继承 IView 父接口的View层对象,这个对象在这里就是View代理对象,P层把更新操作返回给View视图代理对象,再由代理返回给具体V层,从而解耦,并且java的动态代理还有缓存功能,所以这里也是可以对数据进行一次缓存,再V层与它的代理对象绑定时,会直接复用代理对象中缓存的数据进行展示。java动态代理使用到了反射机制,具体实现看博客中
四、mvp使用引发的一些思考
P层可以不持有V层引用而完成视图更新吗?P层如何复用,让多个V能复用同一个P,从而减少代码量?M层是单一职责的数据获取功能,是否可以复用?
git
它是一种分布式的版本控制系统,电脑装了git它就是一个本地仓库,也可以作为中央仓库给其他主机提供版本管理,所以分布式好处是不用时刻联网,代码可以先提交到本地仓库,有网络时再提交到远程仓库,git是一种系统,而github、gitLab,git码云就是别人做的远程仓库,你可以把代码放到他们那里管理保存,git使用镜像的保存方法。
一、安装以及配置
window系统直接下个exe包一键安装,然后会有一个git bash的图标,这就证明git已经装好了,也可以使用git GUI视图界面进行操作,然后可给git的设置“身份证”,通过git bash输入指令 git config --global 配置全局的参数,如下:
●$ git config --global user.name"Your Name"
●$ git config --global user.email"email@example.com"
二、本地仓库
总结:Git管理的是修改而不是文件,commit时只会对add到暂存区的修改进行提交,如果文件多次add再commit将会合并修改的内容再提交。
git的有本地仓库和远程仓库,两者是通过关联才有连系的(每次提交都是先操作本地仓库然后才push到远程仓库),提交本地仓库过程是:从工作区先add到暂存区,再commit将暂存区中的提交到版本库中默认master分支;以下是在git bash里输入指令,“+”号只是表示命令后面接的指令,从$符号后面开始输入就可以了
1、创建:●$ mkdir + 非中文的文件夹名,创建一个文件夹,再这个文件夹目录下再使用 ●$ git init 把它变成Git可以管理的仓库,成功后文件夹目录中生成.git文件。
2、提交:●$ git add + <文件名.后缀>,这个过程可以add多个文件,接着●$ git commit -m +“说明” 将这些add了的文件提交的仓库中,-m 后面是对此次操作的解释说明,便于别人查看理解。
3、回退:●$ git reset -- hard HEAD^ ,HEAD表示当前版本,^表示上一个版本,^^则表示上上一个版本;也可以●$ git reset -- hard +版本号sha1值的前几位 ,这样可以精确还原到某一版本号,HEAD就向一个指针一样指向当前所在的版本。
4、丢弃、撤销修改:●$ git checkout -- file(一定要加“- -”)丢弃工作区中提交的修改;●$ git reset HEAD file 丢弃暂存区中提交的修改,再执行前一步就接着丢弃工作区的修改;如果已经commit到版本库,只能通过版本回退了;如果提交到了远程仓库就没办法丢弃了。
5、删除版本库文件:●$ git rm +文件名,接着●$ git commit 提交到版本库,让版本库执行删除;如果commit之前发现误删了,可以使用 ●$ git checkout -- 文件名,进行撤销。
6、其他的一些命令 ●$git status 查看仓库状态;●$ git diff 比对文件不同,查看做了什么修改;●$ git log 查看历史记录;●$git log --pretty=oneline可以让日志单行显示;●$ git reflog 查看输入命令的记录;
三、远程仓库
本地git要先生成一个ssh公钥和密钥(通过指令 ●$ ssh-keygen -t rsa -C "邮箱@example.com"),因为git向远程仓库提交代码使用SSH协议,即https协议传输,所以要把自己公钥给远程仓库,如在github或gitLab的官网注册帐号后并设置该公钥,以后就可以直接建立SSH链接了。
1、gitHub网页中创建一个新的仓库,成功会得到一个 git@github 的地址,有两种方式和本地仓库管理:一种是本地仓库通过指令●$ git remote add origin + git@github地址 关联起来;另一种是从gitHub仓库中克隆出一个新的仓库到本地,可以在新文件夹中右键打开git bash,通过●$ git clone + git@github地址 ,或者在gitHub直接点击clone按钮下载到本地新文件夹,这样文件夹中就会得到.git文件,再把以前的本地文件夹中内容复制进来。
2、提交:●$ git push -u origin master,这是第一次推送到远程并关联master分支,平常只要commit到本地仓库后,通过●$ git push origin master 就可以推送到远程仓库了
3、拉取:●$ git pull 拉取远程分支,这是push之前必须要做的,因为pull后有冲突可以直接在本地解决,解决以后再push,如果提示“本地分支和远程分支没有关联”,可以●$ git branch --set-upstream branch-本地分支名 origin/branch-远程分支名 ,关联起来。
4、其他命令:●$ git remote 查看远程仓库信息;
四、分支
master是默认创建的主分支,我们可以新建分支进行提交,这样就不影响主分支,更不用担心自己的代码没完成不敢提交,怕影响到其他功能了。
1、分支的命令:git branch;创建分支:git branch <name>; 切换分支:git checkout <name>;创建+切换分支:git checkout -b <name>;合并某分支到当前分支:git merge <name>;删除分支:git branch -d <name> 。
2、合并分支:git merge ,将其他分支和主分支合并,如果有重复或者冲突的地方,需要和同事商讨后合并代码,解决冲突后再合并提交
3、解决冲突conflict:合并时如果有冲突,会给与CONFLICT提示并告诉你哪里有冲突,可以打开文件看到,git分别注解了不同版本的代码,手动解决后再提交,再合并。通过●$ git merge --no-ff 分支名 ;即合并时 ““--no-ff ””会禁用快速合并机制,这样合并后就分支就不会被删除丢弃。
4、隐藏分支stash :如自己开发的还没完成,要做一个紧急需求或者bug修复,可将自己的分支先隐藏,通过 ●$ git stash 隐藏当前分支,再切换到主分支,创建临时新分支,完成后再切换到主分支进行合并,紧急需求完成后再回到之前的隐藏分支,通过●$ git stash apply 或者 ●$ git stash pop恢复。
5、强行删除:未合并的分支只能通过 ●$ git branch -D 文件名, 来强行删除
五、标签
对于某一个版本,可以使用tag更加方便我们去寻找对应的版本库,因为git commit的是SHA1生成的16进制字符串,所以用tag与之关联,方便管理。通过 ●$ git tag <标签名> 创建tag;●$ git tag 查看所有标签;●$ git tag <版本name> <id号> 可以指定给某个版本打标签;●$ git show<标签名> 查看具体信息
六、studio 使用git
通过视图操作,间接的执行了git的命令,所以studio的操作都一样,注意push前先pull最新代码,然后要add,再commit,最后push到对应的分支。
网友评论