第一章:Android基石——四大组件
- 四大组件:
• Activity:负责UI元素的加载与页面之间的跳转,相当于一个页面单元。
• Service:负责与UI无关的操作,如后台耗时操作等。
• ContentProvider:负责存储、共享数据,使得数据在多个应用之间共享。
• Broadcast:在各个应用、组件之间进行通信,简化Android开发中的通信问题。 - Activity构成:Activity -> PhoneWindow -> DecorView -> title+content -> 自己的xml布局。(Decor布局:LinearLayou -> ViewStub、FrameLayout(TextView(title))、FrameLayout(content))
- Activity四种启动模式:
• standard:默认模式,同一个任务栈中可以有多个实例。
• singleTop:如果已经在栈顶,则不创建新的实例,回调onNewIntent()
函数,否则创建新的实例。
• singleTask:如果栈中已有该实例,则会将实例上方activity全部出栈,回调onNewIntent()
函数,多个栈则可以有有个实例。
• singleInstance:会启动一个单独的task存储该实例,并回调onNewIntent()
函数,多个应用之间共享该栈保证只有一个实例。 - Service默认在UI线程执行,生命周期会随着进程而存在,进程被杀掉则会停止(异常杀死会重建,仅重建Service),因此耗时操作需要在service里单开线程执行。
stopService()
或stopSelf()
可关闭service,startService()
执行多次只会有一个service实例。 - IntentService的
onHandleIntent()
会单开工作线程执行操作,执行完毕自动进行stopSelf()
自我销毁,适用于完成一些短期的耗时任务。 - 系统内存不足时Service还是有可能会被回收,但前台服务不会被回收,还可以开启一个通知栏展示。
startForeground(NOTIFY_ID,notification)
设为前台服务。 - AIDL概述:
• AIDL是一种接口描述语言,用于进程间通信。编译器会根据AIDL文件生成对应的java类,通过预先定义的接口和Binder机制达到进程间通信目的。
• 客户端通过调用bindService()
来与远程服务建立一个连接,该链接建立时会返回一个IBinder对象,这是远程服务端Binder的BinderProxy,客户端通过Stub.asInterface()
将该BinderProxy包装成本地的Proxy,远程BinderProxy作为本地Proxy的mRemote
字段,通过mRemote
执行远程函数的调用(transact),远程的Stub的onTransact()
方法会被回调来执行逻辑(远程调客户端也是一样,双向的)。
• 远程service需设置export="true"
和process=":remote"
。(类似socket通信的过程) - 四种广播类型:
• 普通广播:消息传递效率高,但所有receivers接收的顺序不稳定,不可中断广播,直到没有接收器接受为止。(全局类型,只要注册了且过滤条件相同,都能收到)
• 有序广播:通过sendOrderdBroadcast()
来发送,接收器可设置android:priority
优先级,通过setResult()
和getResult()
实现往下传递广播,通过abortBroadcast()
可中断广播。
• 本地广播:使用LocalBroadcastManager.getInstance().sendBroadcast()
发送,只有本进程可以收到广播,为了程序的安全性,在不需要其他进程接收广播时建议使用本地广播。
• Sticky广播:使用sendStickBroadcast()
实现,需要BROADCAST_STICKY权限,广播会一直滞留,只要有新的接收器被注册就会被接收(重复只会保留最后一条广播),可以通过removeStickyBroadcast()
移除广播。 - ContentProvider统一了数据访问方式,是对SQliteOpenHelper的进一步封装,通过Uri来映射要操作的表,提供统一API接口,实现应用间数据共享。Manifest里要设置
authoritys="com.xxx.xxx"
唯一标识。 - Uri表示绝对路径,格式为:
schema + authority + path + id
。 - *表示匹配任意长度的任意字符,#表示匹配任意长度的数字。
第二章:UI——View与动画
- ListView通过Adapter模式、观察者模式、Item复用机制实现了高效的列表显示。其内部有个
AdapterDataSetObserver
,setAdapter()
的时候将其实例化并设置到adapter中,当adapter.notrifyDataSetChanged()
时会通知观察者,listView内部的observer便接收变更通知并控制listView来requestLayout()
重新布局、绘制等。 - RecyclerView对ListView的Adapter再次封装,将判断是否有缓存的逻辑封装到了RecyclerView内部;另外通过桥接的方式将布局职责抽离出去由LayoutManager来控制,使得更加灵活;还可以用ItemDecotation为Item添加装饰等。这些都是将各种职责抽象为接口,并提供一些默认实现,相互注入,由接口实现来完成各项职责,因此用户也能够自己实现接口完成一些自定义的需求。
- 三种自定义控件类型:继承自View完全自定义、继承自现有控件实现特殊效果、继承自ViewGroup实现布局组合。
- 自定义控件核心三步:
测量(onMeasure) -> 布局(onLayout) -> 绘制(onDraw)
。 - MeasureSpec是一个32位的值,有specSize和specMode共同组成,高2位表示Mode,低30位表示Size。
- 三种SpecMode类型:EXACTLY(固定高度和match_parent)、AT_MOST(小于最大高度wrap_content)、UNSPECIFIED(无限制任意大小)。
- MeasureSpec的测量:
• 父View的MeasureSpec由父View的LayoutParams和窗口的长宽size决定。
• 子View的MeasureSpec是由父View的Spec和子View的自己的LayoutParams共同决定的(主要还是看子View的params,如果是固定值dp,则一定是childSize+EXACTLY;如果是wrap,则一定是parentSize+AT_MOST;如果是match就再结合父Spec来决定)。 - Canvas的
save()
和restore()
要配对使用,restore()
可以比save()
少,不能多,否则会报错,想想也是这样。 - Scroller介绍:
• Scoller本身是计算辅助类,只计算滚动位置,不会改变布局参数。它封装了滚动时间、要滚动的目标x和y轴以及每个时间点需要滚动到的x和y位置等信息,用户可以通过getCurX()
和getCurY()
来获取当前时刻View应该滚动到的位置,然后使用View自身的scrollTo()
或scrollBy()
方法来真正实现滚动。
• 执行scroll.startScroll()
来开启滑动,然后复写View的computeScroll()
方法,该方法会在View被绘制时回调,判断scroll.computeScrollOffset()
,返回true表示滚动未完成,就继续重绘postInvalidate()
即可。 - Android四种动画:
• 帧动画:在anim目录下创建,animation-list节点,设置duration和drawable即可;直接设置为view的background,然后getBackground()
转为AnimationDrawable,通过animation.start()
开启动画。注意图片不能太大避免内存溢出。
• 补间动画:在anim目录下创建,alpha/scale/translate/rotate/set五种类型,浮点数/num%/num%p分别表示相对原坐标/相对自身/相对父布局移动距离;通过view.startAnimation(anim)
来开启动画。
• 属性动画:在animator目录下创建,在一段时间内不断修改某个属性值,ValueAnimator封装了时间和节点值(只负责计算和监听,可用于计时器实现);ObjectAnimation封装了作用的目标View、作用的属性等(有默认属性,也可以包装自己的属性和setter效果,自己实现属性设置)。
• VectorDrawabl矢量动画:5.0以上,实现略复杂,用法详见博客。 - 属性动画添加变化值监听addUpdateListener、添加动画节点监听setListener。
- ObjectAnimator初始时设置目标对象、目标属性、时长等值,内部会通过计算出各个时间属性应该得到的值,并通过目标属性的
setter
方法进行更新,如果该属性没有setter
方法则通过反射形式更新目标值(它只负责更新属性域的值,自己要实现的就是更新后的变化效果)。 - TypeEvaluator和TimeInterpolator为类型估值器和差值器,前者接口只有一个方法,就是一个计算接口,可以自己实现计算方法,返回每个节点的值(值的类型也是泛型,可以自己定义);后者可以对前者计算的值再进行一次转化,实现非线性变化。都是接口,只定义计算规则,真正的计算由子类或者实现类实现,可以自定义完了设置给ObjectAnimator,会根据自定义的规则得出计算值并设置给目标对象。
- VelocityTracker、Scoller、ValueAnimator等实际上都是对总距离、时间、节点值等一系列属性的计算类封装,属于辅助类,可以帮助我们算出每个节点的值,至于如何使用这些值来进行变换,自己实现即可。
第三章:App流畅度——多线程
- 每个Android应用在启动时会创建一个线程,称为主线程或者UI线程,Android所有默认操作都在这个线程里执行。主线程会关联一个消息队列,因此可以在主线程直接创建Handler,就是在主线程执行的,循环消息是一个死循环保证主线程不会退出。
- Looper、MessageQueue和Handler的关系:
• 一个线程最多创建一个Looper和多个Handler,默认情况下,消息队列只有一个,就是主线程消息队列,因此Handler可以在主线程中直接创建,发的消息就在主线程MessageQueue获取并调用Handler处理。
• Looper在存储于ThreadLocal中,依附于一个线程,Looper创建并管理一个MessageQueue,Handler创建时会先判断Looper是否存在,存在则将Looper的MessageQueue与自己关联,因此Handler创建依赖于先有Looper。
• 因此,Handler发送消息时是将自己作为Message的target发给MessageQueue,Looper循环消息队列时,拿到消息,调用Message的target(实际上就是handler)来dispatch分发Message;因此,可以创建多个Handler向同一个Looper里发消息。
• 由此可见,如果在子线程直接创建Handler会报错,要先创建Looper,再创建Handler,再Looper.loop()
循环消息,才算一个完整的消息机制创建。且Handler不能访问其他线程的MessageQueue,也保证线程安全性。
• 其实也可以在Handler的构造方法里直接传入looper.messageQueue,它就只往该Looper的MessageQueue里发消息,这样Handler构造时就不会报错了,消息也是发给该线程执行的。
• Handler构造时也可以直接传入一个Callback,它会作为mCallback
,处理Message时使用callback.handleMessage()
,不用重写Handler的handleMessage()
方法了。
• Handler使用时可以sendMessage()
然后处理handleMessage()
,也可以直接post(runnable)
,其实最终都是创建了一个Message对象,在Handler处理时会判断如果Message的Runnable不为空就直接调它的run()
方法,否则就调到handleMessage()
去了。
• 子线程要先创建Looper.prepare()
,然后创建Handler,最后再Looper.loop()
循环起来,注意子线程不用时要退出Looper,否则会一直在阻塞中。 - 线程的wait、sleep、join和yield:
• wait():Object的方法,会使线程进入该对象的等待池中,释放该对象锁,只能等待其他线程来调用Object的notify()
和notifyAll()
才能被唤醒,这几个方法必须在synchronized
方法块中执行(要先获取锁才能wait释放锁)。
• sleep():Thread的静态方法,不会改变锁状态,时间结束会自己唤醒自己。因此如果直接sleep()
就sleep了,如果在synchronized
代码块中调用sleep()
,会持有锁sleep。
• join():Thread的实例方法,其他thread调用,插入当前线程,阻塞当前线程先执行自己。
• yield():Thread的静态方法,线程礼让,当前线程调用,阻塞自己先让出时间片,至于谁执行不管,完了自己继续执行。 - Runnable、Callable、Future和FutureTask:
• Runnable:异步任务封装类,无返回值,可以在Thread和线程池中使用,被用来提交异步任务到线程池threadPool.submit(runnable)
。
• Callable:异步任务封装类,有返回值,只能在线程池中使用,被用来提交异步任务到线程池threadPool.submit(callable)
。
• Future:线程池的执行返回结果管理类,线程一旦被启动就不好控制了,threadPool.submit()
的返回Future,可以控制线程池的执行,获取执行结果等。
• FutureTask:实现了Runnalbe和Future的接口,自己可封装业务处理逻辑,然后threadPool.submit(futureTask)
自己,不用依赖于submit的返回值,自己直接获取并处理管理结果。 - 线程池的作用:重用存在的线程、有效控制最大并发数、提供定时、定期、单线程、并发数控制等。三种状态:运行(创建后)、关闭(shutdown,不接受新任务但未执行完的继续执行)、终止(执行完所有任务)。
- 线程池的几个重要参数:
• corePollSize、maximumPoolSize:核心线程数和最大线程数,当收到新任务时,如果当前线程数小于核线程数则直接创建新线程执行;如果核心等于最大数,则不会创建新线程了;如果大于核心线程数小于最大线程数,则将任务加入阻塞队列,如果阻塞队列已满,创建新线程执行,如果最大线程和阻塞队列都满,调用拒绝策略。
• keepAliveTime:当前线程数大于核心线程数小于最大线程数时,空闲线程的存活时间。
• workQueue:阻塞任务队列,线程大于核心线程时先加入任务队列满了再开启新线程,如果是无界队列,则最大线程数和拒绝策略两个参数无用。
• Handler:拒绝策略。
①AbortPolicy:拒绝新任务直接抛异常(默认策略) 。
②CallerRunsPolicy:拒绝新任务加入,如果线程池未关闭,则使之直接在调用线程执行。
③DiscardOldestPolicy删除队列最早任务,不会抛异常。
④DiscardPolicy:直接拒绝加入新任务,不会抛异常。 - 四种常用线程池:CachedThreadPool、FixedThreadPool、SingleThreadPool和ScheduleThreadPool。
- 几种优化策略:
• CopyOnWriteArrayList(并发List,读的时候不同步,写的时候先复制一份写完再合并,占用双倍内存)。
• ConcurrentHashMap(并发Map,分段锁HashMap,提高方法调用效率)。
• BlockingQueue(阻塞队列,只是一种数据结构,内部实现了锁控制,使用put/take方法,会阻塞)。 - 几种常用阻塞队列:
add(e)、element()方法会抛异常;
offer(e)/offer(e,time,unit)、peek()/poll(time,unit)方法返回false或null;
put(e)、take()不会抛异常,是阻塞方法。
• ArrayBlockingQueue:数组实现、线程安全、有界的队列,内部通过互斥锁保护竞争资源,FIFO原则。
• LinkedBlockingQueue:单向链表实现的阻塞队列,FIFO原则,吞吐量一般高于数组队列。
• LinkedBlockingDeque:双向链表实现的双向并发阻塞队列,支持FIFO和FILO,容量可选,默认容量为MAX。
• ConcurrentLinkedQueue:基于链接节点的无界线程安全队列,采用FIFO原则排序。 - 同步锁机制synchronized:是一种粗略锁,可作用于函数、对象、静态函数和Class,作用于函数和this都是只锁该对象(利用对象内部lock),作用于静态函数和Class锁的是该类下所有同步静态方法。获取锁和释放锁都在同一代码块中,使用简单。只有在synchronized代码块中才能
wait()
、notifyAll()
等,因为要先获取锁,类似只有lock.lock()
里才能condation.await()
、condation.singalAll()
等。 - 显示锁ReentrantLock和Condition:更灵活和高效的锁,可以实现条件阻塞,阻塞队列就是使用该实现。但是使用不当可能会死锁,因为获取锁和释放锁可以分开写,注意必须在
finally
代码块中手动释放锁,否则异常后就会死锁。await()
等方法必须在lock()
以后执行(先获取锁才能再释放)。newCondition()
函数可以获取Lock上的一个条件,用于实现不同线程间的通信,解决Object对象锁线程无法通信的问题,每个条件await()
之后,由其他条件来singalAll()
,实现相互唤醒。 - 几种同步概念:
• 信号量Semaphore:一个共享锁,维护一个信号量许可集,线程可以向它acquire()
许可,也可release()
许可,可以控制线程许可集大小来控制同一时刻同步线程数的大小。
• 循环栅栏CyclicBarrier:允许一组线程相互等待,直到达到某一条件后可执行一个特殊Runnable逻辑,完了其余线程再继续执行。
• 闭锁CountDownLatch:await()
阻塞当前线程,先执行其他线程然后countdown()
置位-1,当计数器为0继续执行当前线程,只能用一次。 - 每个对象内都有一个Lock锁,每个对象是创建在当前调用线程中,因此在对象里调用
lock.lock()
或者在线程中调用object.wait()
都能将当前调用线程给阻塞掉。 - AsyncTask注意事项:
• 异步任务实例必须在UI线程中创建(要利用主线程handler);
•excute()
方法必须放在UI线程中调用(内部会判断mState状态);
• 不能在doInBackground()
中更改UI组件信息(线程池中调用的);
• 一个实例只能执行一次,第二次会抛异常(状态标志位已改变)。 - AsyncTask内部有一个核心线程数为5,最大线程数128,阻塞队列为10的线程池。3.0以下并发138个以上个任务会抛异常(线程池拒绝策略是默认的AbortPolicy),3.0以上虽然还是线程池执行,但是会用一个ArrayQueue阻塞队列来提交任务到线程池执行。控制任务阻塞且一个一个提交(单线程执行)。
- 通过两个运行在不同线程的Handler可实现简易的AsyncTask(一个UI线程的handler和一个子线程的handler)。
第四章:HTTP网络请求
- Http是一种应用层协议,下层通过TCP进行可靠的数据传输,能够保证数据的完整性、正确性,而TCP对数据传输控制的优点也体现在Http上,保证数据传输的吞吐量和效率。
- 7种常用Http请求类型:
• PUT:增。向服务器写入资源,使服务器用请求主体来创建一个以请求Url命名的新文档,若已存在则替换掉。
• DELETE:删。删除服务器请求Url指定的资源,参数与GET一样放在Url中。
• POST:改。向服务器传输数据,通常用于提交Html表单数据。
• GET:查。获取服务器中的某个资源,参数放在Url中。Url长度不同浏览器限制不同,HTTP1.1之后长度无限制。
• HEAD:与GET类似,只返回响应首部,用于判断是请求否连通、资源是否存在等,HTTP/1.1规范必须实现HEAD方法。
• TRACE:HTTP会穿过一些防火墙、代理和网关等,会在行程的最后一站返回TRANCE响应,包括请求原始报文和最终响应报文,用于查看请求最终被修改成了什么样子。
• OPTIONS:请求Web服务器告知支持哪些功能。 - HTTP请求报文格式:
• 请求行:请求方法 (空格) URL (空格) HTTP版本 回车换行
• 请求头:key:value 回车换行
• 空行: 回车换行
• 请求体:文本或二进制数据。
-开始标志:--boundary
-内容头部:key:value
-空行:回车换行
-内容主体:字符或二进制
-结束标志:--boundary-- - HTTP响应报文格式:
• 状态行:HTTP版本 (空格) 状态码 (空格) 结果短语 回车换行
• 响应头:key:value 回车换行
• 空行: 回车换行
• 响应体:json字符串或html文档 - 状态码:
• 100~199:请求已接受继续处理。
• 200~299:请求成功。
• 300~399:重定向。
• 400~499:客户端错误。
• 500~599:服务器错误。 - Http是基于TCP的应用层协议,封装了TCP的使用细节,TCP连接是基于流的可靠连接(传输层),它为Http提供了一条可靠的传输通道。TCP的数据是通过IP分组的小数据块来发送的,Http要发送过一条报文时,会以流的形式将报文数据通过一条打开的TCP连接按顺序传输;TCP收到流数据之后,将数据分割成被称作段的小数据块,将段封装在IP分组中进行传输。
- Http协议栈(从低到高):物理层、数据链路层(网络接口)、网络层(IP)、传输层(TCP)、安全层(TSL或SSL)、应用层(HTTP)。
- 对于文件上传,我们需要做的时将文件数据按照Http报文格式输出到Socket的输出流中,这样文件数据就作为Http报文中的一个参数,通常我们如表单上传时Content-Type为
application/x-www-form-urlencoded
,上传文件时的报文Content-Type都设置为multipart/form-data
格式,文件最好Base64编码一下。 - 不管是不同文本还是文件参数,都是构造固定的Http报文格式,变化的只是Content-Type、Content-Transfer-Encoding以及参数格式,最终将这些参数输入到Http请求的Socket输出流中,通过底层的网络协议将数据传输到目标主机上。
第五章:SQLite数据库
- SQLite概述:
• SQLite是一个遵守ACID(原子性、一致性、隔离性和持久性)的关系型数据库,包含在一个很小的C程序中,被集成在用户程序里,使用动态的、弱类型的SQL语法。
• SQLite引擎不是一个应用程序与之通信的独立进程,其库连接到程序中,成为应用的一部分,这个库可以被动态链接,包括定义、表、索引以及数据本身,作为一个单独的、可跨平台使用的文件存储在主机上。
• 它采用写数据时将整个数据文件加锁的设计,保证写操作串行进行,读操作可以多任务同时进行。
• Android在Framework层封装了一层Java接口供我们方便使用SQLite的功能。 - SQLite的局限:
①仅支持部分触发器;
②alert table时只添加和重命名列,不能修改或删除列(一般只能删除+重新创建表实现升级);
③弱类型,不进行类型检查,可以把字符串插入整数列中。 - SQLite模块分为前端解析系统(词法分析器、语法分析器和代码生成器)和后端引擎(虚拟机VM、B/B++树和页面调度程序)。
- SQLite数据类型只有5种:INTEGER(整型)、REAL(浮点型)、TEXT(字符型UTF-8/UTF-16等)、BLOB(完全按输入存放的数据块)、NULL(数值为空)。除了整型主键,任何列可存储任何类型,注意没有Boolean值,可用Integer代替。
- SQLite在创建列和内部存储时,会进行近似转换,最终转化为那5中内置类型。一个text存入integer中,如果前15位可以顺利转换,则认为是无损转换,可以存入,否则就存入text中。
- 如果在一个表中需要将多个字段组合起来成为唯一,可以在SQL语句最后使用
unique(colum1,colum2)
声明,多主键也是一样。 - 在SQLite中,一个表必须有一个主键。默认情况下SQLite表中都含有一个内置主键,它是一个64位的整型字段,称为
rowid
,自增。也可以定义自定义主键,使用autoincrement
自增。 - SQLite从Android2.2版本以后支持外键,为了兼容以前程序,默认没有开启,需要自己在SQLOpenHelper中重写
onOpen()
方法,db.execSQL("PRAGMA foreign_keys=ON;")
开启。 - insert支持批量插入:
•insert into stu (name,age) select name,age from students;
(先创建表在查询插入,升级表时可以用)
•create table stu as select * from students;
(只能导入基本数据,字段约束不能导入,不常用) - 多表连接查询就是内连接(inner join),取交集;左外连接(left outer jion)、右外链接(right outer join)分别以左、右表全表数据为准,无字段值赋空值;全连接取两张表全集,无值赋空值。但是SQLite只支持内连接和左外连接。
eg:select * from table1 inner join table2 on table1.id = table2.id;
- SQLite的alert表功能支持支修改表名和添加表字段两个功能,不能删除字段。表升级时可以先创建一个临时新表,将旧表数据移植、删除旧表,再将新表改名即可。
- 索引:
• 索引是用于加速查询的结构。默认情况下查询条件会扫描所有的行来找匹配的数据,可为查询频率较高的列建立索引,索引能够为指定的列创建一张索引表,每个索引指向特定的记录,可以加快查询速度。
• 索引可以加快查询速度,却会增加数据库体积,且减慢insert、update和delete操作,因此需要权衡优缺点。不能在较小的表、频繁插入更新的表、含有大量NULL值的表上建立索引。
• 当查询条件是与索引字段判等时,数据库使用索引加速,但是如果有多个索引字段,那么出现第一个不是判等的逻辑之后,后续字段就不会使用索引了(仅判等逻辑会使用索引加速)。 - 视图是动态生成的虚拟表,不会被存储到数据库文件中,常用来将复杂的查询语句的结果简化为一个视图结果,下次直接执行
select * from view
即可。(存储一个结果集,只能查询不能修改删除,但可以增加触发器) - 触发器语法:
create trigger name after insert of colums on table begin action.. end;
。触发器插入记录时使用new
,而删除记录时使用old
。 - SQLite查询语句时,返回的数据类型是Cursor,它提供了一个随机读写访问数据库查询结果集的接口,并不是线程安全的,因此多线程访问Cursor的时候要手动进行同步操作。Cursor中定义了一个光标,默认指向第一个数据。
- SQLite默认会为每个插入、更新操作创建一个事务,每次插入、更新结束后立即提交。因此如果批量插入或更新,最好统一手动开启事务和结束事务。注意不要忘记调用
endTransaction()
结束事务。 - ActiveAndroid数据库框架的表对象必须有一个无参的构造函数,每个表对象有内置的id字段,因此自己的字段不能叫id。通过运行时注解与反射来动态生成语句执行Model对象与SQL语句的转换,效率不是很高。而GreenDAO和DBFlow等使用编译时注解,提高了运行效率。
- 注解实际上就是在源码中附带一些额外信息的载体,这些信息在编译或运行时期都可以从Class信息中读取到,从而进行一些额外操作和处理。
- SQLite是一个内嵌式数据库,数据库服务器就寄宿在应用程序内,与客户端程序运行在同一进程,简化了数据库管理,提高了执行效率。它的锁机制为粗粒度的表级锁,最好不要多线程并发操作,可以将对数据库的查询Manager设为单例,使用一个单线程池执行完了通过Handler回调给主线程。
- SQLite并发:
• 同一时间内打开多个Database连接读写,其中一个会失败。
• 当有写操作,其他读操作会被驳回。
• 当有写操作,其他写操作会被驳回。
• 当有开启事务,事务提交之前,其他写操作会被驳回。
• 当有开启事务,事务提交之前,其他事务请求会被驳回。
• 当有读操作,其他写操作会被驳回。
• 读操作之间能够并发执行。
总结:单写多读,保证全局只有一个连接,注意关闭连接时机。
SQLite拥有文件级别的锁,多线程可以同时读,但只有一个能写。多个线程疯狂的访问数据库应该不会崩溃。但是创建多个连接去写数据库,就会有同步问题。SQLiteHelper获取的可读和可读写的数据库实际都是同一个连接,因此全局创建SQLiteHelper对象单例可在Application里开启连接(database也可不用close,App关闭时会自动释放)。 - 在Application里保持SQLiteHelper同步和单例,保证全局只有一个Database连接,在应用关闭时close掉Database。
- SQLite约束:NOT NULL、DEFAULT、UNIQUE、PRIMARY KEY、CHECK。
- UNION字句/运算符用于合并两个或多个SELECT语句结果,不返回重复的行,每个SELECT被选择的列数必须是相同数目和数据类型的,且保证有相同的顺序。UNION ALL可以包含重的行。
- SQLite常用函数:
count()、max()、min()、avg()、sum()、random()、abs()、upper()、lower()、length()、sqlite_version()
等。
第六章:性能优化
- < include/>标签:
• include标签使用中,include原布局可以设置宽、高、id等属性,这些属性和子控件都会直接附加到主布局中,在主布局中可以直接根据id获取这些子布局中的控件。
• 同时在主布局中可以重设include的id,会覆盖掉原布局id,这时候就要先获取到原布局的View,再view.findViewById()
找原布局中的子View。
• 如果要在主布局中要设置include的布局属性,要重写layout_width
和layout_height
两个属性。 - <merge/>标签:
• merge标签与include结合使用,如果相邻两个布局重复则用merge标签优化,只能作为根布局来用,布局直接附加到主布局中。
• 如果用它来inflate一个布局时,要指定parent
元素且attachToRoot
参数要设为true(一定是被加到一个父布局中的)。 - <ViewStub/>:
• ViewStub用于布局延迟加载,只能inflate()
或setVisiable()
一次,ViewStub本身就不存在了,返回inflate后的布局 。
•layout_width
或layout_height
等一些属性要加载ViewStub上,这些在它inflate后会传给初始化布局。
• ViewStub有id
和inflatedId
两个属性,id作用于自身,inflatedId指被inflate()后得到的View的根视图id,寻找子布局控件时要先有子布局View,可以通过inflatedId找到。
• ViewStub目前还不支持merge标签。 - RelativeLayout和LinearLayout的
layout_weight
属性通常会将视图进行两次测量或以上,因此在ListView中避免使用这两种属性。 - 内存优化:
• 避免保留不必要的Service,可以使用IntentService来使Service处理完任务后自动关闭。
•onTeimMemory()
中可以监听TRIM_MEMORY_UI_HIDDEN级别的回调,当UI已经隐藏时,释放UI占用的资源,注意它和onStop()
的区别。
• 在manifest下添加largHeap=true
属性可以声明一个更大的heap空间。(不一定好使)
• 避免Bitmap的浪费,进行像素压缩以及及时回收Bitmap。
• 多使用SparseArray等优化过的容器类,他们避免了对key与value的autobox自动装箱和拆箱的过程。
• 避免过多使用枚举enums,通常枚举的内存消耗是static constants的2倍。
• 使用ProGuard提剔除不需要的代码和资源,进行代码压缩混淆。
• 对打包的pak进行zipalign优化。 - 内存泄漏:垃圾回收机制只回收那些不可达的对象,如果一个本该被销毁的对象被错误的持有,那么就属于内存泄漏,该对象不会被及时回收。可用内存越来越小时会引起大量的GC操作,虽然不会明显影响App性能,但是会使App变慢,降低UI流畅度。
- LeakCanry通过ActivityLifecycleCallbacks监听了Acivity的所有生命周期回调,可以追踪、检测、输入内存泄露的信息,在通知栏和日志上进行输出。Memory Monitor可以检测内存实时使用情况,当已分配内存明显降低说明触发了GC操作,如果短时间内内存急剧上升或者GC十分频繁,这时候要注意是否有内存泄漏风险。
- 打开GPU可以查看过度绘制区域:通常需要解决过度绘制了2次以上的情况(设置了background才算绘制)。
• 无颜色:没有过度绘制,只绘制了1次。
• 蓝色 :过度绘制了1次,一个像素点被绘制了2次。
• 绿色 :过度绘制了2次,一个像素点被绘制了3次。
• 浅红色:过度绘制了3次,一个像素点被绘制了4次。
• 深红色:过度绘制了4次,一个像素点被绘制了5次。 - 数据采集和分析工具TraceView可以分析每个方法的调用次数、耗时、CPU占用率等信息,帮助我们优化代码。
第七章:代码规范
- 常见规范:代码缩进、一句一行、空行分隔、函数排布、修饰词顺序、字段函数命名等。
- 如果一个类的字段没必要对外隐藏,直接设为
public
即可,不需要setter和getter产生额外消耗。 - 重载父类函数最好加上
@Override
注解标识。 - 操作符加上括号标明优先级、if-else等方法加上括号、switch函数加上
default
分支等。
第八章:Git版本控制
- Git是分布式版本控制系统,在客户端将整个项目仓库完整的镜像下来,将变化的文件做快照保存在本地微型文件系统中,若无变化则只对上次的快照做一次连接;通过文件摘要判断是否有过修改,因此只关心文件数据的整体是否发生变化而不关注文件内容的具体差异。
- gitconfig的三种级别:每一种级别会覆盖掉上层级别的设置,如果查看时有2项以上,说明级别不同,取最后一个配置。
①/etc/gitconfig:系统中所有用户的所有仓库普遍适用的配置,适用--system
选项读写的文件。
②~/.gitconfig:用户目录下的配置文件,适用于该用户的所有仓库,适用--global
选项读写的文件。
③项目下的./git/gitconfig:对该项目下配置有效。 - 基本命令:
• git rm --cached [-r] xxx:移除文件的追踪(不删除/删除本地文件)
• git checkout -- xxx:还原工作区当前已修改的文件
• git reset HEAD xxx:还原暂存区add以后的文件到工作区
• git reset --hard HEAD^ / xxx:还原版本库commit以后的文件到上个版本/指定版本
• git clone remote_url:克隆远程项目到本地
• git remote add origin remote_url:先有本地仓库,再与远程已有仓库关联
• git fetch remote branch:下载远程仓库的变动(只下载不合并)
• git pull remote branch:下载远程仓库的变动(合并),第一次拉取实际就自动关联了分支,所以一般都先拉取一次
• git push -u origin master:推送master上的内容到远程仓库(第一次推送 -u关联分支)
• git remote rm remote_url:删除远程仓库
• git push origin locan_branch:remote_branch:推送本地分支到远程,如果远程没有,则新建一个分支
• git push origin :remote_branch:删除上一步创建的那个远程分支 - GitHub生成SSH key命令:
ssh-keygen -t rsa -b 4096 -c "your_email@example.com"
。使用SSH -t git@github.com
来验证github的ssh key是否添加成功。可通过SSH或Https进行通信,一般选择SSH,通过SSH Key访问不用每次都输入密码。 -
git remote add [branch_name] [branch_url]
命令可以关联本地仓库和一个远程仓库,且一个本地方库可以关联多个远程仓库。因此,github的Fork和Pull Request模式就是先将原项目Fork到自己的github下,再在本地添加关联自己Fork来的项目,并进行修改、提交;最后在自己的Fork项目再添加关联原项目(本地仓库串行关联了两个远程仓库),先将原项目pull一次到自己的Fork项目,解决冲突,再在自己Fork的项目上进行pull request即可。
第九章:单元测试
- 单元测试是由开发人员编写,用于检测特定条件下目标代码正确性的代码。是独立的类,基于方法的细粒度的,具有回归性,一劳永逸,将项目重构后只要单元测试能跑通,基本代码就没什么问题。
- Junit是Java的一套基本的单元测试框架:1.定义一个测试类继承自
TestCase.class
。2.重写setUp()
和tearDown()
方法进行必要的初始化逻辑和销毁逻辑。3.自定义public的测试方法,以test
开头,添加@Test
注解。4.方法中可以添加各种基于方法测验证代码。5.运行测试类即可。注意每个测试方法相互并不相关,执行顺序也不一定,相互不要有依赖性。 - 断言类型:
assertEquals、assertTrue/False、assertNull/NotNull、assertSame/NotSame、failNotEquals、failSame/NotSame、fail(String)、fail
等。 - 运行多个测试类的两种方法:
①创建TestSuite类,测试类用JUnit4TestAdapter包装,调用suite.addTest(new JUnit4TestAdapter(AddTest.class))
即可,返回suite实例。
②TestSuite类上添加注解@RunWith(Suite.class)
和@Suite.SuiteClasses(AddTest.calss)
即可。 - 要测试的内容:各种边界条件、if-else的各种执行路径等。
- 当某些关联的模块没有开发完毕时,可以手动Mock虚拟对象来提供测试依赖的实例(基于接口编程的方便性,可创建测试实现类进行替换即可)。
- Mockio库是一套基于MIT协议的开源Java测试框架,能够随时随地自动Mock对象、验证结果以给测试用例提前打桩。它在运行时才去Mock对象,模拟测试对象的行为,消除了依赖,解耦性强。
• 需引入dexmaker-mockito-1.0.jar和dexmaker-1.0.jar两个jar包。
• 常用功能都在org.mockito.Mockit.*类的静态方法里,测试类中可以引入这个类即可。
• Mock对象可以调用mock(xxx.class)
也可以直接在成员变量上加上@Mock
注解来自动创建对象,注解方式要在setUp()
方法中初始化Mock对象MockitoAnnotations.initMocks(this)
。
•verify()
函数可以验证对象的交互;when()...then()...
可以为打测试桩,类似debug中的Evaluate expression
和Set Value
,让用例按照自己设置的值输出;any()
可以输出任意值,方便我们测试。 - Android测试:
• Android平台下的单元测试都是InstrumentationTestCase的子类,内部封装了Instrumentation对四大组件的测试操作以及一些基本操作,它继承自Junit的TestCase,它有一些子类进行更详细的测试使用。
• AndroidTestCase测试类内置了mContext
对象,可以用来启动Activity、获取资源、获取系统服务等。
• ActivityInstrumentationTestCase/TestCase2、ServiceTestCase、ProviderTestCase2等测试类可以测试四大组件,模拟点击效果等。
• ActivityTestCase2中代码不是在UI线程,涉及更新UI的操作需要runTestOnUiThread()
,然后调用getInstrumentation().waitForIdleSync()
执行后续操作。 - 测试驱动TDD开发要求先写测试用例,再进行开发,一步一步验证测试用例,这使得我们开发的时候要多去解耦,抽出功能性代码到单独的类中。
- 常用测试框架:Robolectric、Espresso、UI Automator等。
第十章:六大原则与设计模式
- 六大原则:
• 里氏替换原则:所有引用基类、接口的地方可以透明的试用其子类对象,反过来就不行。(多态的体现,高层调用者持有调用对象的接口引用,可以拓展性的创建具体不同的子类实例)
• 依赖导致原则:模块间的依赖都是通过接口或抽象发生,实现类之间不发生直接的依赖关系。(高层间相互持有对方的接口依赖,使子类可替换性实现而调用者代码不用变)
• 开闭原则 :一个软件的实体如类、模块和函数应该对拓展开放、对修改关闭。(策略模式,面向接口传值,提供不同实现类做具体解析处理)。以上三者都是"面向接口编程"
• 单一职责原则:一个类只做一件事,类中的函数必须是高度相关的。(功能性代码分开写,通过接口来确认功能函数)
• 接口隔离原则:一个类对另一个类的依赖应该建立在一个最小接口上。(一个子类可能实现多个接口,高层调用者只关心其某个单一职责接口,不必知道其他细节实现)
• 迪米特原则 :一个对象应当对其他对象有最少的了解。(一个类应该对自己需要耦合的类知道的最少,或关联最简接口,或只与最直接朋友通信而不管一些代理/helper的存在)。以上三者都是"单一职责" - 六大设计模式最终的几个关键字:抽象、单一职责、最小化;高内聚、低耦合。
- 反模式:与设计模式类似,是一种文字记录形式,描述了对某个问题必然产生的消极后果的常见解决方案,包括形式、主要原因、典型症状、后果以及如何通过重构解决问题等。
- 设计模式总结了在特定问题下的正确的解决方案,而反模式则是告诉你在特定问题上的错误的解决方案以及他们的原因、解决方案,最终通过正确的解决方案将项目拉回正轨。
第十一章:重构
- 常见的重构手法:
• 提取子函数:将臃肿的函数代码分离到具体的职责单一的子函数中,也方便其他业务逻辑调用。
• 上移到父类函数:如果某个函数在较多类中共同调用,应当上移到统一的父类中,可以通过参数不同定义不同实现,必要时子类也可以重写该函数。
• 下移函数到子类:有时候父类与子类中有些函数不一定全部用到,可以建立中间层基类实现局部继承。
• 封装固定的调用逻辑:可以通过接口或父类封装规定的方法调用逻辑,至于具体实现可以不同,但调用逻辑要相同。接口只定义调用逻辑,父类可以封装好一套固定实现。
• 使用泛型去除重复逻辑:必要的时候使用泛型来定义父类,可以更好的抽象代码,复用逻辑,减少代码量。
• 使用对象避免过多的参数:当一个方法参数过多时应当考虑将参数封装成一个对象来传递,这样更加清晰且默认的null
字段不用显示声明了。
• 转移函数:根据业务逻辑将函数转移到主要使用它的类中,单一职责原则。
• 将类型码转为状态模式:当使用过多条件时,想添加一种新的条件时,秩序建立一个新的子类,并在其中实现相应的函数,在适当的时机将该子类注入到对象中即可(策略模式)。状态父类中定义了所有行为方法(非抽象);不用状态的子类选择性的实现需要实现的方法,代理了行为类;行为类中定义状态父类,选择性的注入子类实例,行为方法完全调用不同子类实例的代理方法实现(状态模式)。
• NullObject模式:有时候一个类中的某个对象,供其他类调用时都要先判空,可以在该类中定义一个private static final的默认的null实现类,其他对象调用get时返回默认null实现类或真正的实现类,这样其他类只管调用拿到的对象即可,肯定不为空。
• 分解“胖”类型:尽量保持类的职责单一,将一个功能性的类拆分成多个不同功能的类组合,这些不同功能的类也可以使用工厂方法实现可替换性。