1.计算时间 微信金额是拆的时候实时算出来,不是预先分配的,采用的是纯内存计算,不需要预算空间存储。 采取实时计算金额的考虑:预算需要占存储,实时效率很高,预算才效率低。
2.分配算法 随机产生,额度在0.01-(剩余平均值 * 2)之间。 假如发1块钱,总共10个红包,均值是0.1元一个,那么发出来的额度在0.01~0.2之间波动,当前面4个包被领取0.7元时,剩下0.3,总共6个包,那么发出来的红包额度在0.01~0.1之间波动,这样算下去,会超过最开始的全部金额,因此到了后面如果不够这么算,会采取算法,保证剩余用户至少能拿到1分钱,如果前面的人手气不好,后面的余额越多,红包额度也就越多,因此实际概率一样的。
3.每一个红包的概率不是绝对均等,就是一个简单的拍脑袋算法,有可能出现金额一样的,但是手气最佳只有一个,先抢到的为手气最佳。
4.代码简易实现:
//最后一个人之前的红包总额
public class RedPacketInfo {
//剩余红包数目
public int remainNum;
//剩余金额
public double remainMoney;
//红包总额
public double moneySum;
//抢红包人数
public int redNum;
}
private double previousSum;
/**
*
* @param index 第几个人
* @param info 红包info
* @return
*/
public synchronized String getRandomMoney(int index,RedPacketInfo info){
//保留两位小数
DecimalFormat df = new DecimalFormat("0.00");
//最后一个红包应该是红包总额减去前面所有红包总额
if(info.remainNum == 1){
previousSum = Double.valueOf(df.format(previousSum));
System.out.println("previousSum: "+previousSum);
info.remainNum--;
String money = df.format(info.moneySum - previousSum);
return Double.valueOf(money) <=0 ? "0.01" : money;
}
Random r = new Random();
//红包最小金额
double min = 0.01;
//红包最大金额
double max = info.remainMoney / info.remainNum * 2;
//计算红包大小
double money = r.nextDouble() * max;
money = money <= min ? 0.01 : money;
// money = Math.floor(money * 100) / 100;
info.remainNum --;
info.remainMoney -= money;
if(index < info.redNum){
previousSum += Double.valueOf(df.format(money));
}
return df.format(money);
}
@Test
public void test(){
RedPacketInfo info = new RedPacketInfo();
//初始化
info.remainNum = 30;
info.remainMoney = 200;
info.moneySum = 200;
info.redNum = 30;
//计算后的红包总额
double sum = 0;
for (int i = 1; i <= info.redNum; i++) {
String randomMoney = getRandomMoney(i,info);
System.out.println("第 "+i + "人抢到的红包金额:"+randomMoney);
sum+=Double.valueOf(randomMoney);
}
System.out.println("红包总额:"+Math.round(sum));
}
1. handler,Looper,MessageQueue,Message知识点总结:
1).在主线程中可以直接创建Handler对象,因为在ActivityThread类的main方法调用了Looper.prepareMainLooper()方法,
在子线程中创建Handler对象前需要先调用Looper.preare()方法创建Looper对象,在Handler的构造方法中会调用
Looper.myLooper()方法获取Looper对象,如果Looper对象为空,就会抛出异常;
2).每一个线程中最多只能有一个Looper对象,否则抛异常,可以通过Looper.myLooper()获取当前线程的Looper实例,
通过Looper.getMainLooper()获取主(UI)线程的Looper实例。
3).一个线程中只能有一个Looper对象和一个消息队列MessageQueue对象,但是可以有多个Handler对象;
4).Message:Handler接收和处理消息的对象。
5).Looper:它的loop方法负责读取消息队列MessageQueue中的消息,读到消息后把消息发送给Handler进行处理。
6).MessageQueue:消息队列,它采用先进先出的方式来管理Message,程序在调用Looper.prepare()创建Looper对象的时候,
会在Looper的构造方法中同时创建消息队列MessageQueue对象,一个Looper只能对应一个消息队列MessageQueue对象。
7).MessageQueue主要包含了两种操作,插入和读取,而读取操作本身也会伴随着删除操作,插入和读取对应的分别是enqueueMessage和next。
8).Handler:它的作用有两个分别是发送消息和处理消息。
9).Looper对象负责管理MessageQueue,而MessageQueue主要是用来存放handler发送的消息
10).Looper.loop():启动looper中的循环线程,Handler就会从消息队列里取消息并进行对应处理。要注意的是写在Looper.loop()之后的代码不会被执行,
这个函数内部应该是一个循环,当调用mHandler.getLooper().quit()后,loop()才会中止,其后的代码才能得以运行。
11).子线程中使用Handler发送消息处理消息
1. 子线程中必须先创建Looper
Looper.prepare();
2. 创建Handler对象,复写handlerMessage方法处理消息10
3. 启动looper循环
Looper.loop();
12).主线程中直接可以创建Handler对象,因为在ActivityThread类的main方法中调用了Looper.prepareMainLooper()方法,
启动了looper循环Looper.loop(),这样就能很方便的进行消息发送和接受处理了。
13)Looper的quit方法和quitSafely方法有什么区别,可以看出实际上是调用的MessageQueue的quit方法
public void quit() {
mQueue.quit(false);
}
public void quitSafely() {
mQueue.quit(true);
}
无论是调用了quit方法还是quitSafely方法,MessageQueue将不再接收新的Message,此时消息循环就结束,MessageQueued的next方法将返回null,结束loop()的死循环.
这时候再通过Handler调用sendMessage或post等方法发送消息时均返回false,表示消息没有成功放入消息队列MessageQueue中,因为消息队列已经退出了。
2. Handler,Looper,MessageQueue消息处理流程
1).当我们使用handler调用sendEmptyMessage(int)发送消息时,Handler内部会去调用enqueueMessage(MessageQueue queue,Message msg)方法
把发送的消息添加到消息队列MessageQueue中,在这个方法中同时还会设置msg.target=this此时就把当前handler对象绑定到msg.target中了,
这样就完成了Handler向消息队列存放消息的过程
// 最后调用此方法添加到消息队列中
private boolean enqueueMessage(MessageQueue queue, Message msg,
long uptimeMillis) {
msg.target = this;// 设置发送目标对象是Handler本身
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);// 添加到消息队列中
}
2).Looper取消息和分发消息的主要逻辑在loop方法里,通过loop()方法内部源码我们可以知道,首先会通过myLoooper()去获取一个Looper对象,
如果Looper对象为null,就会报出一个我们非常熟悉的错误提示,“No Looper;Looper.prepare() wasn't called on this thread”,
要求我们先通过Looper.prepare()方法去创建Looper对象;如果Looper不为null,那么就会去获取消息队列MessageQueue对象,
接着就进入一个for的死循环,不断从消息队列MessageQueue对象中获取消息,如果消息不为空,那么就会调用
msg.target的dispatchMessage(Message)方法去处理消息,那么这个target又是什么,没错target就是我们创建的Handler对象。
//looper中最重要的方法loop(),该方法是个死循环,
//会不断去消息队列MessageQueue中获取消息,
//然后调dispatchMessage(msg)方法去执行
public static void loop() {
final Looper me = myLooper();
//保证当前线程必须有Looper对象,如果没有则抛出异常,调用Looper.loop()之前应该先调用Looper.prepare().
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
//Looper需要不断从MessageQueue中取出消息,所以它持有MessageQueue对象
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {
//这里开始执行死循环,queue通过调用next方法来取出下一个消息。
//很多人很疑惑死循环不会相当耗费性能吗,如果没有那么多消息怎么办?
//其实当没有消息的时候,next方法会阻塞在这里,不会往下执行了,性能问题不存在。
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
//这里满足了死循环跳出的条件,即取出的消息为null
//没有消息next不是会阻塞吗,怎么会返回null呢?
//其实只有MessageQueue停止的时候(调用quit方法),才会返回null
//MessageQueue停止后,调用next返回null,且不再接受新消息,下面还有详细介绍。
return;
}
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
//这里的msg.target是Handler对象,分发消息到Handler去执行。
//有人问主线程可以创建这么多Handler,怎么保证这个Handler发送的消息不会跑到其它Handler去执行呢?
//那是因为在发送Message时,他会绑定发送的Handler,在此处分发消息时,也只会回调发送该条消息的Handler。
//那么分发消息具体在哪个线程执行呢?
//我觉得这个不该问,那当然是当前方法在哪个线程调用就在哪个线程执行啦。
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
//这里对Message对象进行回收,会清空所有之前Message设置的数据。
//正是因为Message有回收机制,我们在创建消息的时候应该优先选择Message.obtain().
//如果发送的消息足够多,Message缓存的Message对象不够了,obtain内部会调用new Message()创建一个新的对象。
msg.recycleUnchecked();
}
}
3). 在dispatchMessage方法中的逻辑比较简单,具体就是如果mCallback不为空,则调用mCallback的handleMessage()方法,
否则直接调用Handler的handleMessage()方法,并将消息对象作为参数传递过去。
/**
* Handle system messages here.
*/
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
4).因为我们的Handler是在主线程创建的,也就是说Looper也是主线程的Looper,因此handleMessage内部处理最终都会在主线程上执行,
就这样整个流程都执行完了。
5)ActivityThread的main源码
public final class ActivityThread {
...
public static void main(String[] args) {
SamplingProfilerIntegration.start();
// CloseGuard defaults to true and can be quite spammy. We
// disable it here, but selectively enable it later (via
// StrictMode) on debug builds, but using DropBox, not logs.
CloseGuard.setEnabled(false);
Environment.initForCurrentUser();
// Set the reporter for event logging in libcore
EventLogger.setReporter(new EventLoggingReporter());
Security.addProvider(new AndroidKeyStoreProvider());
// Make sure TrustedCertificateStore looks in the right place for CA certificates
final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
TrustedCertificateStore.setDefaultUserDirectory(configDir);
Process.setArgV0("");
//注意这里,这里创建了主线程的Looper
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
AsyncTask.init();
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
//开启消息循环
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
}
3.handler内存泄漏处理
方案一:通过程序逻辑来进行保护
1. 在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。
2. 如果你的Handler是被delay的Message持有了引用,那么使用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。
方案二:将Handler声明为静态类
静态类不持有外部类的对象,所以你的Activity可以随意被回收。代码如下:
static class TestHandler extends Handler {
WeakReference mActivityReference;
TestHandler(Activity activity) {
mActivityReference= new WeakReference(activity);
}
@Override
public void handleMessage(Message msg) {
final Activity activity = mActivityReference.get();
if (activity != null) {
mImageView.setImageBitmap(mBitmap);
}
}
}
1、如果事件不被中断,整个事件流向是一个类U型图
2、dispatchTouchEvent 和 onTouchEvent方法return true是终结事件传递。return false 是回溯到父View的onTouchEvent方法。
3、dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent
ViewGroup 和View的这些方法的默认实现就是会让整个事件安装U型完整走完,所以 return super.xxxxxx() 就会让事件依照U型的方向的完整走完整个事件流动路径),
中间不做任何改动,不回溯、不终止,每个环节都走到。
4、onInterceptTouchEvent方法中 return true就会交给自己的onTouchEvent的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,
因为子View也需要这个事件,所以onInterceptTouchEvent拦截器return super.onInterceptTouchEvent()和return false是一样的,是不会拦截的,
事件会继续往子View的dispatchTouchEvent传递。
5、ViewGroup 的dispatchTouchEvent,之前说的return true是终结传递。return false 是回溯到父View的onTouchEvent,
然后ViewGroup怎样通过dispatchTouchEvent方法能把事件分发到自己的onTouchEvent处理呢,return true和false 都不行,那么只能通过Interceptor
把事件拦截下来给自己的onTouchEvent,所以ViewGroup dispatchTouchEvent方法的super默认实现就是去调用onInterceptTouchEvent,记住这一点。
6、View 没有拦截器,为了让View可以把事件分发给自己的onTouchEvent,View的dispatchTouchEvent默认实现(super)就是把事件分发给自己的onTouchEvent。
7、ViewGroup和View 的dispatchTouchEvent是做事件分发,那么这个事件可能分发出去的四个目标
1)、自己消费,终结传递。------->return true ;
2)、给自己的onTouchEvent处理------->调用super.dispatchTouchEvent()系统默认会去调用onInterceptTouchEvent,
在onInterceptTouchEvent return true就会去把事件分给自己的onTouchEvent处理。
3)、传给子View------>调用super.dispatchTouchEvent()默认实现会去调用 onInterceptTouchEvent 在onInterceptTouchEvent return false,就会把事件传给子View。
4)、不传给子View,事件终止往下传递,事件开始回溯,从父View的onTouchEvent开始事件从下到上回归执行每个控件的onTouchEvent------->return false;
8、ViewGroup和View的onTouchEvent方法是做事件处理的,那么这个事件只能有两个处理方式:
1)、自己消费掉,事件终结,不再传给谁----->return true;
2)、继续从下往上传,不消费事件,让父View也能收到到这个事件----->return false;
9、ViewGroup的onInterceptTouchEvent方法对于事件有两种情况:
1)、拦截下来,给自己的onTouchEvent处理--->return true;
2)、不拦截,把事件往下传给子View---->return false,ViewGroup默认是不拦截的,所以super==false;【
1. Measure过程
MeasureSpec是一个大小跟模式的组合值,MeasureSpec中的值是一个整型(32位)将size和mode打包成一个Int型,
其中高两位是mode,后面30位存的是size,是为了减少对象的分配开支。
MeasureSpec一共有三种模式
UPSPECIFIED : 父容器对于子容器没有任何限制,子容器想要多大就多大
EXACTLY: 父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值,
它对应于LayoutParams 中的 match_parent 和具体的数值这两种模式
AT_MOST:父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。
它对应于 LayoutParams 中的 wrap_content。
一、onMeasure测量
1. 如果父View的MeasureSpec 是EXACTLY,说明父View的大小是确切的,(确切的意思很好理解,如果
一个View的MeasureSpec 是EXACTLY,那么它的size 是多大,最后展示到屏幕就一定是那么大)
1、如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是确切,那么子View的大小肯定是确切的,
而且大小值就是父View的size。所以子View的size=父View的size,mode=EXACTLY
2、如果子View 的layout_xxxx是WRAP_CONTENT,也就是子View的大小是根据自己的content来决定的,子View的大小没有确切的值,
子View的大小最大为父View的大小,不能超过父View的大小,子View MeasureSpec mode的应该是AT_MOST
3、如果如果子View 的layout_xxxx是确定的值(188dp),那么就更简单了,不管你父View的mode和size是什么,我都写死了就是188dp,
那么控件最后展示就是188dp,不管我的父View有多大,也不管我自己的content 有多大,反正我就是这么大,
所以这种情况MeasureSpec的mode = EXACTLY,大小size=你在layout_xxxx写死的值。
2. 如果父View的MeasureSpec是AT_MOST,说明父View的大小是不确定,最大的大小是MeasureSpec的size值,不能超过这个值。
1、如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是不确定(只知道最大只能多大),子View的大小MATCH_PARENT(充满整个父View),
那么子View你即使充满父容器,你的大小也是不确定的,因为父View自己都确定不了自己的大小,你MATCH_PARENT你的大小
肯定也不能确定的,所以子View的mode=AT_MOST,size=父View的size。
2、如果子View 的layout_xxxx是WRAP_CONTENT,父View的大小是不确定(只知道最大只能多大),子View又是WRAP_CONTENT,
那么在子View的content没算出大小之前,子View的大小最大就是父View的大小,所以子View MeasureSpec mode的就是AT_MOST。
3、如果子View 的layout_xxxx是确定的值(188dp),同上,写多少就是多少,改变不了的。
3. 如果父View的MeasureSpec 是UNSPECIFIED(未指定),表示没有任何束缚和约束,不像AT_MOST表示最大只能多大,
不也像EXACTLY表示父View确定的大小,子View可以得到任意想要的大小,不受约束
1、如果子View 的layout_xxxx是MATCH_PARENT,因为父View的MeasureSpec是UNSPECIFIED,父View自己的大小并没有任何约束和要求,
那么对于子View来说无论是WRAP_CONTENT还是MATCH_PARENT,子View也是没有任何束缚的,想多大就多大,
没有不能超过多少的要求,一旦没有任何要求和约束,size的值就没有任何意义了,所以一般都直接设置成0
2、如果子View 的layout_xxxx是确定的值(188dp),同上,写多少就是多少,改变不了的(记住,只要子View设置的是确切值,
那么无论怎么测量,大小都是不变的,都是你写的那个值)
总结:
1.当子View 采用固定宽高时,不管父容器的测量模式是什么,子View的测量模式都是EXACTLY(精确模式),并且大小是写死的宽高。
2.当子View 的宽高是match_parent时,如果父容器的模式是EXACTLY(精确模式),那么子View也是EXACTLY,并且大小就是父容器的宽高;
如果父容器是AT_MOST(最大模式),那么子View也是AT_MOST,并且大小是不会超过父容器的大小。
3.当子View 的宽高是wrap_content时,不管父容器的模式是EXACTLY还是AT_MOST模式,子View的模式总是AT_MOST模式,并且大小不超过父容器的大小。
二、onLayout布局,目的就是安排其children在父视图的具体位置
onLayout配合onMeasure方法一起使用可以实现自定义View的复杂布局。自定义View首先调用onMeasure进行测量,
然后调用onLayout方法动态获取子View和子View的测量大小,然后进行layout布局。重载onLayout的目的就是安排其children在父View的具体位置,
重载onLayout通常做法就是写一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置。
三、onDraw视图绘制,View的绘制是借助onDraw方法传入的Canvas类来进行的。
1. 绘制背景:background.draw(canvas);
2. 对View的内容进行绘制:onDraw();
3. 绘制子View:dispatchDraw();
4. 绘制滚动条:onDrawScrollBars。
参考博客Java-冒泡排序 和 博客 Java-快速排序
http协议(超文本传输协议):是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议.
HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。
HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统
主要特点:
1、简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST
2、灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记
3. 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
4. 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,
这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
5、 支持B/S及C/S模式。
HTTP使用统一资源标识符(Uniform Resource Identifiers, URI)来传输数据和建立连接。URL是一种特殊类型的URI,包含了用于查找某个资源的足够的信息
URL,全称是UniformResourceLocator, 中文叫统一资源定位符,是互联网上用来标识某一处资源的地址
URI,是uniform resource identifier,统一资源标识符,用来唯一的标识一个资源
URI一般由三部组成:
①访问资源的命名机制
②存放资源的主机名
③资源自身的名称,由路径表示,着重强调于资源。
URL是uniform resource locator,统一资源定位器,它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。
URL一般由三部组成:
①协议(或称为服务方式)
②存有该资源的主机IP地址(有时也包括端口号)
③主机资源的具体地址。如目录和文件名等
HTTP之请求消息Request
请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。
HTTP之响应消息Response
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
HTTP之状态码
200 OK //客户端请求成功
400 Bad Request //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden //服务器收到请求,但是拒绝提供服务
404 Not Found //请求资源不存在,eg:输入了错误的URL
500 Internal Server Error //服务器发生不可预期的错误
503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
GET和POST请求的区别
1 GET提交,请求的数据会附在URL之后(就是把数据放置在HTTP协议头中),以?分割URL和传输数据,多个参数用&连接;
POST提交:把提交的数据放置在是HTTP包的包体中。因此,GET提交的数据会在地址栏中显示出来,而POST提交,地址栏不会改变
2.传输数据大小
因此对于GET提交时,传输数据就会受到URL长度的 限制。
POST:由于不是通过URL传值,理论上数据不受限。
3.安全性
POST的安全性要比GET的安全性高。比如:通过GET提交数据,用户名和密码将明文出现在URL上,
使用GET提交数据还可能会造成Cross-site request forgery攻击
4.GET方式需要使用Request.QueryString来取得变量的值,而POST方式通过Request.Form来获取变量的值。
Https单双向验证参考博客HTTPS单双向验证
(1)在app的build.gradle中开启minifyEnabled混淆代码,在app/proguard-rules.pro编写混淆规则,根据自己项目依赖的库一一混淆
(2)开启shrinkResources去除无用资源,shrinkResources依赖于minifyEnabled,必须和minifyEnabled一起用,
就是打开shrinkResources也必须打开minifyEnabled。
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
}
}
}
(3)删除未使用到xml和图片,使用Android Studio的Lint,步骤:
Android Studio -> Menu -> Refactor -> Remove Unused Resources
选择 Refactor 一键删除
选择 Perview 预览未使用到的资源
(4)删除未使用到代码,点击菜单栏 Analyze -> Run Inspection by Name -> unused declaration -> Moudule ‘app’ -> OK
(5)png图片格式转成jpg,缩小大图
(6) 使用vector
(7) 使用shape作为背景,很多点击效果可能会使用到图片,可以换成shape实现
(8) 使用微信Android资源混淆工具
(9) 使用webp格式
(10) 一个APK尽量只用一套图片,从内存占用和适配的角度考虑,取720p的资源,放到xhdpi目录。
(11) 删除无用的语言资源
(12) 使用tinypng有损压缩
(13) 删除多余的so文件,
比如armable-v7包下的so,基本上armable的so也是兼容armable-v7的,armable-v7a的库会对图形渲染方面有很大的改进,如果没有这方面的要求,可以精简。
比如删除x86包下的so,x86包下的so在x86型号的手机是需要的,如果产品没用这方面的要求也可以精简。
建议实际工作的配置是只保留armable、armable-x86下的so文件,算是一个折中的方案。
(14)避免重复库,使用更小的库。
(15) 代码优化
图片压缩方式:
(1) 质量压缩
public static Bitmap compressImageByQuality(Bitmap bitmap, int limitMaxSize){
//进行有损压缩
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int options = 100; //0-100 100表示不压缩
bitmap.compress(Bitmap.CompressFormat.JPEG,options,baos);
//循环判断如果压缩后图片是否大于limitMaxSize,大于继续压缩
int baosLength = baos.toByteArray().length;
while ((baosLength / 1024) > limitMaxSize){
//重置baos即让下一次的写入覆盖之前的内容
baos.reset();
options = options - 10 > 0 ? options - 10 : 0;
bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);
if(options == 0){
break;
}
}
Log.e("compressImage options",options+"");
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());// 把压缩后的数据baos存放到ByteArrayInputStream中
Bitmap outBitmap = BitmapFactory.decodeStream(isBm, null, null);// 把ByteArrayInputStream数据生成图片
return outBitmap;
}
(2) 按比例大小压缩,将options.inJustDecodeBounds设置为true,在解码过程中就不会申请内存去创建Bitmap,返回的是一个空的Bitmap,
但是可以获取图片的一些属性,例如图片宽高,图片类型等等。
public static Bitmap compressImageByScale(Context context,int resId,int limitMaxSize){
float outWidth = 720f;
float outHeight = 1280f;
return compressImageByScale(context,resId,outWidth,outHeight,limitMaxSize);
}
public static Bitmap compressImageByScale(Context context,int resId, float outWidth, float outHeight,int limitMaxSize){
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 设置为true,不将图片解码到内存中
options.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeResource(context.getResources(), resId, options);
options.inSampleSize = countInSampleSize(options,outWidth,outHeight);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resId, options);
return bitmap;
}
public static int countInSampleSize(BitmapFactory.Options options,float outWidth,float outHeight){
//获取原始图片宽高
int srcWidth = options.outWidth;
int srcHeight = options.outHeight;
int inSampleSize = 1;
// 如果宽度大的话根据宽度固定大小缩放 否则根据高度固定大小缩放
if(srcWidth > outWidth && srcWidth > srcHeight){
inSampleSize = (int) (srcWidth / outWidth);
}else if(srcHeight > outHeight && srcHeight > srcWidth){
inSampleSize = (int) (srcHeight / outHeight);
}
return inSampleSize < 0 ? 1 : inSampleSize;
}
(3)先按比例压缩 再质量压缩
(4) JNI终极压缩
参考博客“内存优化小结”
写法一:线程安全加锁
public class Single {
private Single(){}
public static Single single;
public static Single getSingle(){
if(single == null){
synchronized (Single.class){
if(single == null){
single = new Single();
}
}
}
return single;
}
}
写法二:内部类形式
public class Single {
private Single(){}
public static Single getSingle(){
return SingleHolder.single;
}
private static class SingleHolder{
private final static Single single = new Single();
}
}
1.添加相应架包
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
compile 'io.reactivex.rxjava2:rxjava:2.0.1'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'com.squareup.okhttp3:logging-interceptor:3.6.0'
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
2. 创建RetrofitHelper帮助类,这个类是单例的,里面主要是配置缓存文件和目录,配置OkHttpClient,设置网络请求连接,读取和写入等的超时时间,
添加日志过滤器,然后通过Retrofit.Builder()这个类,通过build模式,添加Gson支持,添加RxJava支持,关联okhttp,返回Retrofit对象,
通过Retrofit这个对象的create方法创建一个HttpService接口对象,这个接口里面主要定义的是一些网络请求接口信息,
这个帮助类的主要目的是提供一个方法返回这个HttpService接口对象,用于后面的网络请求。
3.创建HttpService接口对象,用于定义app数据请求接口信息。
4.创建一个HttpUtils工具类,这个类主要是用于所有网络请求的,里面主要是提供post和get请求,以及检测版本更新等方法
(1)创建一个内部接口,定义一个网络请求成功和请求失败的方法,请求成功方法参数直接传入Gson解析之后的对象,请求失败方法参数传入错误信息。
(2)定义一个订阅事件的方法,因为rx进行网络请求都是链式结构,通过Observable设置网络请求在子线程,设置ui更新在主线程,然后通过subscribe方法把被观察者和观察者连接起来。
//订阅事件
public static void setSubscriber(Observable observable, Observer observer){
observable.subscribeOn(Schedulers.io()) //开启新线程
.observeOn(AndroidSchedulers.mainThread()) //ui操作放主线程
.subscribe(observer);
}
(3)在post方法里面调用这个方法,传入被观察者Observable对象和观察者Observer对象,因为我是和MVP结合使用的,所以我把Observer又封装了一个类BaseObserver
(4) 创建BaseObserver实现Observer接口,构造函数中传入自定义MVP的View接口BaseIView,实现Observer的几个方法,同时定义一个请求成功和请求失败的抽象方法,
请求成功方法参数直接传入Gson解析之后的对象,请求失败方法参数传入错误信息,在onNext方法里面调用网络请求成功的方法,
在onError方法里面调用请求失败的方法,同时调用BaseIView接口的请求失败错误处理方法,在onComplete方法里面调用BaseIView接口的请求完成关闭Loading等的方法。
(5)在post方法里面通过setSubscriber方法,传入被观察者Observable对象和BaseObserver对象,实现刚刚定义的请求成功和失败的抽象方法,
在请求成功的方法里面调用HttpUtils里面定义的内部接口的请求成功方法,在请求失败方法里面调用内部接口的请求失败方法,处理各种错误码。
5. 创建BaseIView接口,(所有的View接口都会实现这个接口)定义数据请求失败,数据请求成功关闭Loading的方法。
6. 创建BaseInfo,里面主要定义接口通用的字段,所有定义的bean类都会继承这个父类,同时也是为了多态的方便
7. 创建BasePresenter基类,构造方法中传BaseIView接口,同时通过RetrofitHelper提供的静态方法获取HttpService接口对象,
在这个类里面主要定义一个抽象方法public abstract void initData()用于子类在这个方法进行网络请求操作,
同时还定义一个onDestoryView用于在Activity或者Fragment生命周期结束时,把View接口置为null,切断数据联系。
8.详细封装代码可以参考我的博文:Rxjava2 Retrofit2 和 mvp再封装
整个库分为 RequestManager(请求管理器),Engine(数据获取引擎)、 Fetcher(数据获取器)、MemoryCache(内存缓存)、
DiskLRUCache、Transformation(图片处理)、Encoder(本地缓存存储)、Registry(图片类型及解析器配置)、Target(目标) 等模块。
简单的讲就是 Glide 收到加载及显示资源的任务,创建 Request 并将它交给RequestManager,Request 启动
Engine 去数据源获取资源(通过 Fetcher ),获取到后 Transformation 处理后交给 Target。
Glide 依赖于 DiskLRUCache、GifDecoder 等开源库去完成本地缓存和 Gif 图片解码工作。
glide优点:
1)图片request跟随Activity或Fragment生命周期。Activity onStart()时,request得到恢复;Activity onStop()时,request会被暂停;
Activity onDestroy()时,request会被清除。这个功能对用户完全透明,故而用户完全不用担心Activity退出而request仍然存在等内存泄露问题。
2)采用LruCache和DiskLruCache两级缓存,分别对应内存缓存和磁盘缓存
3)支持丰富的图片格式,支持GIF,webp等格式
4)保存到磁盘上的图片是经过裁剪和压缩的,故存储数据小
5)采用BitmapPool对象池管理BitmapResource的创建和回收,采用线程池管理执行耗时request的子线程。
可以减少对象的创建和垃圾回收,并控制对象个数以保持足够的空余内存。
6)简单易用,Glide.with(context).load(url).into(imageView),采用builder模式,这是常见的使用方式。
用户只需要知道如何利用builder传入参数即可。Glide的很多特性对用户来说完全透明,用户不需要去操心。
1. 第一步:with()方法
with()方法是Glide类中的一组静态方法,有如下几个with()方法的方法重载:
//with方法的方法重载
public static RequestManager with(Context context) {
//得到一个RequestManagerRetriever对象
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(context);
}
public static RequestManager with(Activity activity) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(activity);
}
public static RequestManager with(FragmentActivity activity) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(activity);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static RequestManager with(android.app.Fragment fragment) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(fragment);
}
public static RequestManager with(Fragment fragment) {
RequestManagerRetriever retriever = RequestManagerRetriever.get();
return retriever.get(fragment);
}
retriever.get()是这部分的关键点,获取了RequestManager对象。RequestManager主要作用为实现
request和Activity生命周期相关联。在本文讲述的Glide这部分内容中关系不大。
2. 第二步: load()方法
创建DrawableTypeRequest并利用它的load方法设置相关参数,这部分关键点是new DrawableTypeRequest(),
DrawableTypeRequest是GenericRequestBuilder的子类,故load()调用后builder就创建好了。之后就可以通过builder模式来配置参数了
3. 第三步:into()方法
a) 创建Request对象并将builder中设入的参数,设置到Request数据类中
b)调用 requestTracker中的runRequest(),发送request请求。创建好request只是into()方法的第一阶段,还需要将request请求发送出去。
into()方法是比较关键也比较麻烦的一个方法。Glide将View封装为了ViewTarget。into()方法分为request创建和request发送两大部分。
创建采用Builder模式生成GenericRequest对象,发送则调用GenericRequest的begin方法,最终回调到底层Engine部分的相关方法。
可以看出,经过Glide的层层封装,复杂的逻辑变得相对简单了很多。
4.绑定生命周期
Glide中一个重要特性是Request可以随Activity或Fragment的onStart而resume,onStop而pause,onDestroy而clear,
从而节约流量和内存,并且防止内存泄露,这一切都由Glide在内部实现了。用户唯一要注意的是,Glide.with()方法中
尽量传入Activity或Fragment,而不是Application,不然没办法进行生命周期管理。后面分绑定流程和生命周期调用流程
来分析整个实现原理,实现这一切的核心类如下。
1)RequestManager,实现了LifeCycleListener,主要作用为结合Activity或Fragment生命周期,对Request进行管理,
如pauseRequests(), resumeRequests(), clearRequests()
2) RequestManagerRetriever,获取RequestManager,和SupportRequestManagerFragment,并将二者绑定,
从而在fragment的生命周期方法中可回调到RequestManager对request进行生命周期管理的相应方法。
3) SupportRequestManagerFragment, 空白Fragment,与RequestManager进行了绑定,
作用为提供Fragment生命周期管理方法入口,如onStart(), onStop(), onDestroy()。
4)ActivityFragmentLifecycle, 管理LifecycleListener, 空白Fragment会回调它的onStart(), onStop(), onDestroy()
5)LifecycleListener,接口,定义生命周期管理方法,onStart(), onStop(), onDestroy(). RequestManager实现了它。
Glide中巧妙的用一个空白Fragment来实现了生命周期调用,并使用LifeCycleListener来回调相应的request管理方法。
设计模式上也很规范,是一个典型的MVC,SupportRequestManagerFragment用来接入生命周期方法,RequestManager用来实现生命周期中处理request的方法,
RequestManagerRetriever用来绑定二者,作为桥梁。我们在网络请求request中同样可以用此方法来实现生命周期绑定。
垃圾回收的意义:
在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;而在Java中,当没有对象引用指向原先分配给某个对象的内存时,
该内存便成为垃圾。JVM的一个系统级线程会自动释放该内存块。垃圾回收意味着程序不再需要的对象是"无用信息",这些信息将被丢弃。
当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。事实上,除了释放没用的对象,垃圾回收也可以清除内存记录碎片。
由于创建对象和垃圾回收器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。
碎片整理将所占用的堆内存移到堆的一端,JVM将整理出的内存分配给新的对象。
垃圾回收能自动释放内存空间,减轻编程的负担。这使Java 虚拟机具有一些优点。首先,它能使编程效率提高。
在没有垃圾回收机制的时候,可能要花许多时间来解决一个难懂的存储器问题。在用Java语言编程的时候,靠垃圾回收机制可大大缩短时间。
其次是它保护程序的完整性, 垃圾回收是Java语言安全性策略的一个重要部份。
垃圾回收的一个潜在的缺点是它的开销影响程序性能。Java虚拟机必须追踪运行程序中有用的对象,而且最终释放没用的对象。
这一个过程需要花费处理器的时间。其次垃圾回收算法的不完备性,早先采用的某些垃圾回收算法就不能保证100%收集到所有的废弃内存。
当然随着垃圾回收算法的不断改进以及软硬件运行效率的不断提升,这些问题都可以迎刃而解。
垃圾收集算法:
1.引用计数法
引用计数法是唯一没有使用根集的垃圾回收的法,该算法使用引用计数器来区分存活对象和不再使用的对象。一般来说,堆中的每个对象对应一个引用计数器。
当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1当对象出了作用域后(该对象丢弃不再使用),
引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。
2. 标记 -清除算法
“标记-清除”算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
3.复制算法
“复制”算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,
就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
4.标记-整理算法
“标记-整理”算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,
而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
5.分代收集算法
“分代收集”算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,
每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
减少GC开销的措施
(1)不要显式调用System.gc()
(2)尽量减少临时对象的使用
(3)对象不用时最好显式置为Null
(4)尽量使用StringBuffer,而不用String来累加字符串
(5)能用基本类型如Int,long,就不用Integer,Long对象
(6)尽量少用静态对象变量
(7)分散对象创建或删除的时间
垃圾收集器分类
1、Serial收集器(复制算法)
2、ParNew收集器(停止-复制算法)
3、Parallel收集器(停止-复制算法)
4、Parallel Old 收集器(停止-复制算法)
5、CMS收集器(标记-清理算法)
6、G1收集器(标记整理算法)
1. 在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,
即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,
当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
2. 两个重要的参数:容量(Capacity)和负载因子(Load factor),HashMap最多只允许一条记录的键为null,允许多条记录的值为null。
3. put函数的实现
1).判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
2).根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
3).判断table[i]的首个元素是否和key一样,如果相同用新的value直接覆盖旧的value,否则转向④,这里的相同指的是hashCode以及equals;
4).判断table[i] 是否为红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
5).遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;
遍历过程中若发现key已经存在,用新的value直接覆盖旧的value即可;
6).插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
代码实现:
public V put(K key, V value) {
// 对key的hashCode()做hash
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
// 步骤①:tab为空则创建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步骤②:计算index,并对null做处理
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
// 步骤③:节点key存在,直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 步骤④:判断该链为红黑树
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
// 步骤⑤:该链为链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//链表长度大于8转换为红黑树进行处理
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// key已经存在直接覆盖value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 写入
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 步骤⑥:超过最大容量 就扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
4.get函数的实现
大致思路如下:
1. 计算hash&(n-1)得到在链表数组中的位置first=tab[hash&(n-1)],先判断first的key是否与参数key相等,如果相等(命中),直接返回该key对应的value值;
2. 如果不等,则通过key.equals(k)去查找对应的entry,
若为红黑树,则在树中通过key.equals(k)查找,O(logn);
若为链表,则在链表中通过key.equals(k)查找,O(n)。
代码实现:
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 直接命中
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 未命中
if ((e = first.next) != null) {
// 在树中get
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
// 在链表中get
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
5. hash函数的实现,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,
这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
在对hashCode()计算hash时具体实现是这样的:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
6.resize的实现
当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。在resize的过程,
简单的说就是把bucket扩充为2倍,之后重新计算index,把节点再放到新的bucket中。
代码实现:
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 超过最大值就不再扩充了,就只好随你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 没超过最大值,就扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 把每个bucket都移动到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
// 原索引
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
7. HashMap工作原理
首先有一个数组,数组的每个元素都是链表,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,
但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,
同一个链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。
8. 小结
(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。
(4) JDK1.8引入红黑树大程度优化了HashMap的性能。
(5) 还没升级JDK1.8的,现在开始升级吧。HashMap的性能提升仅仅是JDK1.8的冰山一角。
(1)standard标准模式: 这个模式是默认的启动模式,即标准模式,在不指定启动模式的前提下,系统默认使用该模式启动Activity,
每次启动一个Activity都会重写创建一个新的实例,不管这个实例存不存在。
(2)singleTop-栈顶复用模式:这个模式下,如果新的activity已经位于栈顶,那么这个Activity不会被重写创建,同时它的onNewIntent方法会被调用,
通过此方法的参数我们可以去除当前请求的信息。如果栈顶不存在该Activity的实例,则情况与standard模式相同。
singleTop模式分3种情况
当前栈中已有该Activity的实例并且该实例位于栈顶时,不会新建实例,而是复用栈顶的实例,并且会将Intent对象传入,回调onNewIntent方法
当前栈中已有该Activity的实例但是该实例不在栈顶时,其行为和standard启动模式一样,依然会创建一个新的实例
当前栈中不存在该Activity的实例时,其行为同standard启动模式
(3)singleTask-栈内复用模式:在这个模式下,如果栈中存在这个Activity的实例就会复用这个Activity,不管它是否位于栈顶,复用时,
会将它上面的Activity全部出栈,并且会回调该实例的onNewIntent方法。其实这个过程还存在一个任务栈的匹配,因为这个模式启动时,
会在自己需要的任务栈中寻找实例,这个任务栈就是通过taskAffinity属性指定。如果这个任务栈不存在,则会创建这个任务栈。
(4)singleInstance-全局唯一模式:该模式具备singleTask模式的所有特性外,与它的区别就是,这种模式下的Activity会单独占用一个Task栈,
具有全局唯一性,即整个系统中就这么一个实例,由于栈内复用的特性,后续的请求均不会创建新的Activity实例,除非这个特殊的任务栈被销毁了。
以singleInstance模式启动的Activity在整个系统中是单例的,如果在启动这样的Activiyt时,已经存在了一个实例,那么会把它所在的任务调度到前台,重用这个实例。
参考我的博客 scheme唤醒外部APP
//开启
Intent intent = new Intent();
intent.setData(Uri.parse("");
//目标app的目标activity需要在清单文件中注册 指定具体协议名称和主机类等信息。
数据存储方式:文件,sp,数据库,网络 内容观察者
数据解析方式:xml解析:dom解析,sax解析,pull解析; json解析:谷歌自带json解析,Gson,fastJson ,JackJson。