Android之layoutinflate学习

前言:

layoutinflater是我们处理view时经常用到的方法,如往布局里动态添加view,recycleview的item创建,自定义view的构造方法中…
所以这篇文章就是来好好介绍学习一下
view inflate(int resource, ViewGroup root, boolean attachToRoot)
这个方法

rootlayout根布局和LayoutParams

在学习layoutInflater之前,我们必须先了解一下这两个知识

RootLayout

根布局其实就是ViewGroup的最外面的一层布局,他也是相对的概念,他是相对他子view的根布局,每个view都是加在一个容器里,也就是viewgroup。

1
2
3
4
5
6
7
8
9
10
11
12
13
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_alignParentBottom="true">


<Button
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimaryDark"/>

</LinearLayout>

这段代码中LinearLayout就是button的根布局,而将这个整个加入到另一布局中,如:
relativeLayout.addview(linearlayout)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/root_layout">


<TextView
android:layout_width="wrap_content"
android:layout_height="60dp"
android:text="@string/app_name"
android:layout_alignParentEnd="true"
/>

</RelativeLayout>

那么这时RelativeLayout就是LinearLayout的根布局。
那么根布局有什么用呢?别急,你还需要学习一下LayoutParams,才能弄明白。

LayoutParams

Layout Parameters :布局参数。其实我们平时再用xml写布局时也一直在写他:如上面button的layout_width,layout_height,都是他的layoutparams,即xml里所以的layout_***都是在设置该view的layoutparams。接下来看一下官方解释:

1、LayoutParams are used by views to tell their parents how they want to be laid out.
LayoutParams是View用来告诉它的父控件如何放置自己的
2、The base LayoutParams class just describes how big the view wants to be for both width and height.
基类LayoutParams(也就是ViewGroup.LayoutParams)仅仅描述了这个View想要的宽度和高度
3、There are subclasses of LayoutParams for different subclasses of
ViewGroup.
不同ViewGroup的继承类对应着不同的ViewGroup.LayoutParams的子类

所以,layoutparams是其实设置的是该view的父布局(根布局)提供给他的LayoutParams,然后,根布局就会根据子布局设置的layoutParams作出相应的处理,来给他宽高和应该放置的位置,
如上面代码中linearlayout设置了layout_alignParentBottom="true" ,这其实就是relativeLayout的LayoutParams的一个属性,然后当把他加入relativelayout后,就会把它放到alignParentBottom
LayoutParams.png

LayoutInflater

明白前面俩概念后,再学习layoutinflater就很简单了。 他就有几个重载函数,但最后都指向了下面这个函数:
LayoutInflater.inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

参数解释:

  • resource: 就是布局资源id,如R.layout.item_test,即要动态添加的布局的xml
  • root :就是之前所讲的根布局,其实这个是设置根布局的意思,并不是就把上一个参数resource指定的view添加了这个root根布局里,注意只是设置,给这个view设置了他的相应根布局的LayoutParams。
    可以设置为Null。如果为Null,那新布局(第一个参数)的根布局参数(注意:是新视图的根布局)就不会被设置成任何布局参数,只有等添加到父布局时候,重新赋给这个根布局新的布局参数,并且第三个参数将毫无作用。我们在第三个参数讲解第二个参数在非Null情况。这时会调用generateDefaultLayoutParams()方法,不同个viewgroup会重写这个方法,从而给其子布局设置不同的默认布局参数。
  • attachToRoot: 这个参数才是设置是否要把view加入到root中,如果为true,则直接把view加入到root中,如果为false,则不会添加,等待后期自己手动addview添加

如果我们不需在onCreateView()中将View添加进ViewGroup,为什么还要传入ViewGroup呢?为什么inflate()方法必须要传入根ViewGroup?
原因是及时不需要马上将新填充的View添加进ViewGroup,我们还是需要这个父元素的LayoutParams来在将来添加时决定View的size和position。

总结

所以通过上面的解释,可看出,root参数十分重要,一定要正确赋值,否则可能会导致有些layout_不起作用,或布局的宽高位置跟你设想的不一样。一般有以下几个使用建议:

如果可以传入ViewGroup作为根元素,那就传入它。
避免将null作为根ViewGroup传入。
当我们不负责将layout文件的View添加进ViewGroup时设置attachToRoot参数为false。
不要在View已经被添加进ViewGroup时传入true。
自定义View时很适合将attachToRoot设置为true。

Java List集合使用小结

1.关于循环遍历

平时我们最常使用的两个List:

  • ArrayList
  • LinkedList

很熟悉的特点一个是数组实现,一个是链表
也就是一个能随机存取,一个只能顺序存取,所以在循环遍历时要十分注意:
Java中有大体三种遍历方式:

  • 最基本for循环
  • 迭代器遍历
  • foreach
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    List<Integer> list = new ArrayList<>();
    list.add(5);
    list.add(23);
    list.add(42);
    for (int i = 0; i < list.size(); i++) {
    System.out.print(list.get(i) + ",");
    }

    Iterator it = list.iterator();
    while (it.hasNext()) {
    System.out.print(it.next() + ",");
    }

    for (Integer i : list) {
    System.out.print(i + ",");
    }

    关于foreach:

    注意,他可不只是比for方便一点,他的实现上是跟for循环是有很大区别的
    其实for each循环内部也是依赖于Iterator迭代器,只不过Java提供的语法糖,Java编译器会将其转化为Iterator迭代器方式遍历。对以下for each循环进行反编译:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     for (Integer i : list) {
    System.out.println(i);
    }

    反编译后:

    Integer i;
    for(Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println(i)){
    i = (Integer)iterator.next();
    }

    foreach使用限制:

  • 适用对象:遍历数组、集合(实现了Iterator接口)、Iterable对象
  • 在用foreach循环遍历一个集合时不能向集合中增加元素,不能从集合中删除元素,否则会抛出ConcurrentModificationException异常。抛出该异常是因为在集合内部有一个modCount变量用于记录集合中元素的个数,当向集合中增加或删除元素时,modCount也会随之变化,在遍历开始时会记录modCount的值,每次遍历元素时都会判断该变量是否发生了变化,如果发生了变化则抛出ConcurrentModificationException异常,所以这时如果想改变,就老老实实用Iterator对象去遍历,用Iterator的方法去增删
    1
    2
    3
    4
    5
    6
    7
    正确用法
    Iterator<Student> stuIter = students.iterator();
    while (stuIter.hasNext()) {
    Student student = stuIter.next();
    if (student.getId() == 2)
    stuIter.remove();//这里要使用Iterator的remove方法移除当前对象,如果使用List的remove方法,则同样会出现ConcurrentModificationException
    }
  • 当使用foreach循环基本类型时变量时不能修改集合中的元素的值,遍历对象时可以修改对象的属性的值,但是不能修改对象的引用
    修改基本类型的值(原集合中的值没有变化,因为str是集合中变量的一个副本,只是一个句柄)
  • 最后一点时很容易看出的,foreach循环没有index,也就是没有下标,如果需要就还是用for把

选择:

当用linkedlist的时候,肯定要用后面两种遍历方法,第一种直接for循环效率极低

参考:
Java中的增强for循环(for each)的实现原理与坑
java中for和foreach的区别

2.关于java中的堆栈stack和队列实现的选取

不难发现java中也有stack直接实现类:Stack<T> stack = new Stack<>()
但是当你点进去看源码注释的时候发现:

A more complete and consistent set of LIFO stack operations isprovided by the {@link Deque} interface and its implementations, whichshould be used in preference to this class. For example: {@code Deque stack = new ArrayDeque();}

官方告诉我们有更好的实现,就是用deque,至于为什么弃用stack,可参考一下网上结论(大致就是stack设计得不严谨,不规范)

Stack是继承自Vector,Vector是由数组实现的集合类,他包含了大量集合处理的方法。而Stack之所以继承Vector,是为了复用Vector中的方法,来实现进栈(push)、出栈(pop)等操作。这里就是Stack设计不好的地方,既然只是为了实现栈,不用链表来单独实现,而是为了复用简单的方法而迫使它继承Vector,Stack和Vector本来是毫无关系的。这使得Stack在基于数组实现上效率受影响,另外因为继承Vector类,Stack可以复用Vector大量方法,这使得Stack在设计上不严谨

所以既然这样,那就用deque来做吧,deque是一个双向队列,完全具备普通队列FIFO的功能,同时它也具备了Stack的LIFO功能,并且保留了push和pop函数,所以使用起来应该是一点障碍都没有。
这里我想说一下他的两个实现:

  • LinkedList
  • ArrayDeque

一个是基于数组,一个是基于链表,所以在选择上还是视具体情况而定

Android之AsyncTask源码学习

AsyncTask

1.定义,介绍:

  • asynctask是Android中的一个自带的轻量级异步类,通过他可以轻松的实现工作线程和UI线程之间的通信和线程切换(其实也只能在工作和ui线程之间切换,稍后会提到)
  • asynctask是一个抽象类,所以我们需要创建他的子类,一般重写他的四个方法即可:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //这个就是要在后台做的工作,他将运行在后台工作线程上
    @WorkerThread
    protected abstract Result doInBackground(Params... params);

    //这个时开始执行前的操作,运行在主线程上
    @MainThread
    protected void onPreExecute() ;

    //doInBackground完成后调用
    @MainThread
    protected void onPostExecute(Result result)

    //实时更新,通过在doInBackground中调用publishProgress()方法
    @MainThread
    protected void onProgressUpdate(Progress... values)

2.源码

1.创建:

  • 问题:为什么说只能在工作线程和UI线程之间通信?我自己建一个looper线程也不行吗?(当然这种情况也很少)
    看他的三个构造函数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
     public AsyncTask() {
    this((Looper) null);
    }

    /**
    * Creates a new asynchronous task. This constructor must be invoked on the UI thread.
    *
    * @hide
    */
    public AsyncTask(@Nullable Handler handler) {
    this(handler != null ? handler.getLooper() : null);
    }

    /**
    * Creates a new asynchronous task. This constructor must be invoked on the UI thread.
    *
    * @hide
    */
    public AsyncTask(@Nullable Looper callbackLooper) {
    mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
    ? getMainHandler()
    : new Handler(callbackLooper);

    ...
    ...
    ...
    }
    可以看出不管调用哪个都会指向第三个构造方法,关键代码
    `mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
    ? getMainHandler()
    : new Handler(callbackLooper);  `  
    所以,如果调用第一个构造函数,那么传入的Looper为null;如果调用第二个则传入handler.getLooper(),如果他为MainLooper的话,也将和第一个构造函数一样进入了 getMainHandler()方法:创建了一个拥有主线程的InternalHandler
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    private static Handler getMainHandler() {
    synchronized (AsyncTask.class) {
    if (sHandler == null) {
    sHandler = new InternalHandler(Looper.getMainLooper());
    }
    return sHandler;
    }
    }

    private static class InternalHandler extends Handler {
    public InternalHandler(Looper looper) {
    super(looper);
    }

    @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
    @Override
    public void handleMessage(Message msg) {
    AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
    switch (msg.what) {
    case MESSAGE_POST_RESULT:
    // There is only one result
    result.mTask.finish(result.mData[0]);
    break;
    case MESSAGE_POST_PROGRESS:
    result.mTask.onProgressUpdate(result.mData);
    break;
    }
    }
    }
    那如果他不是mainlooper的话,就创建了一个new Handler(callbackLooper),然而这个handle既没有传入callback,也没有重写其handleMessage方法,其实是一个空的handle,但他可以调用msg的callback呀。就算这样,那我是不是仍然可以创建一个不与主线程(UI线程)相连的handle,那为啥还说只能用在ui与工作线程之前呢?
    哈哈,请再看一下上面贴上的三个构造函数的源码,我专门把第二个和第三个的注释也贴了上去,有一个很重要的注解@hide,也就是说google把第二个和第三个构造函数给隐藏起来了,不对开发者开放,我们根本访问不到(虽然能看到,而且时public,感兴趣可以查一下这个注解)。所以我们只能用第一个构造函数,传入空的looper,从而调用getMainHandler();
  • 顺便提一下handle 的执行顺序(dispatchMessage)
    1. msg的callback不为空,调用handleCallback方法(message.callback.run())
    2. mCallback不为空,调用mCallback.handleMessage(msg)
    3. 最后如果其他都为空,执行Handler自身的 handleMessage(msg) 方法

2.执行:

  • 包装doInBackground方法。
    创建完handle后,在第三个构造函数中可以看出,他接着创建了work(实现了callable)和futur(FutureTask对象其实就是更方便操作线程,Futrue可以监视目标线程调用call的情况,当你调用Future的get()方法以获得结果时,当前线程就开始阻塞,直接call方法结束返回结果。)其中很关键的代码就是在work中调用了,然后又将work用futuretask封装,在之后的调用中会最后提交给线程池
    result = doInBackground(mParams);
    mFuture = new FutureTask<Result>(mWorker) {......}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    mWorker = new WorkerRunnable<Params, Result>() {
    public Result call() throws Exception {
    mTaskInvoked.set(true);
    Result result = null;
    try {
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    //noinspection unchecked
    result = doInBackground(mParams);
    Binder.flushPendingCommands();
    } catch (Throwable tr) {
    mCancelled.set(true);
    throw tr;
    } finally {
    postResult(result);
    }
    return result;
    }
    };
    最后在finaly里调用postResult(result),将结果传递给handle,将调用handle(InternalHandler)的handleMessage,他处理两个,一个是执行结束finish(),从而调用了onPostExecute,另一个是在doInBackground里用户调用的publishProgress像handle发送消息,从而调用onProgressUpdate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void handleMessage(Message msg) {
AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
switch (msg.what) {
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
}
}

protected final void publishProgress(Progress... values) {
if (!isCancelled()) {
getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
new AsyncTaskResult<Progress>(this, values)).sendToTarget();
}
}

private void finish(Result result) {
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}

asynctask的两个线程池:

- `public static final Executor THREAD_POOL_EXECUTOR`:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);


/**
* An {@link Executor} that can be used to execute tasks in parallel.
*/
public static final Executor THREAD_POOL_EXECUTOR;

static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}

可以看出,这是一个核心线程数最小为2的线程池,它是用来并行执行task的,但当达到核心线程最大值后,依旧会在阻塞队列里等待

  • public static final Executor SERIAL_EXECUTOR = new SerialExecutor()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    private static class SerialExecutor implements Executor {
    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;

    public synchronized void execute(final Runnable r) {
    mTasks.offer(new Runnable() {
    public void run() {
    try {
    r.run();
    } finally {
    scheduleNext();
    }
    }
    });
    if (mActive == null) {
    scheduleNext();
    }
    }

    protected synchronized void scheduleNext() {
    if ((mActive = mTasks.poll()) != null) {
    THREAD_POOL_EXECUTOR.execute(mActive);
    }
    }
    }

    所以第二个 是一个顺序串行执行的Executor:每次将任务提交到数组双向队列里,execute后会依次执行,然后scheduleNext获取下一个任务。

  • AsyncTask中默认时使用的第二个,SERIAL_EXECUTOR,串行执行,
    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
    当然你也可以修改

    1
    2
    3
    4
    /** @hide */
    public static void setDefaultExecutor(Executor exec) {
    sDefaultExecutor = exec;
    }

    然而很不幸,他也被hide起来了…但是可以在接下来的入口处,直接调用executeOnExecutor(Executor exec,Params... params)方法传入指定的Executor

开始执行的入口:

当创建完asynctask后,调用asynctask.execute(Params… params)就可开始执行,然后其实是调用了executeOnExecutor(Executor exec,Params... params)这个方法,传入 sDefaultExecutor和doInbackgroud方法的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException("Cannot execute task:"
+ " the task is already running.");
case FINISHED:
throw new IllegalStateException("Cannot execute task:"
+ " the task has already been executed "
+ "(a task can be executed only once)");
}
}

mStatus = Status.RUNNING;

onPreExecute();

mWorker.mParams = params;
exec.execute(mFuture);

return this;
}

四个回调执行的线程

- `onPreExecute()`:

从上面代码我们可以看到在这里执行了 onPreExecute(),所以,在哪个线程执行了asynctask.execute(Params… params), onPreExecute()他就在哪个线程执行
- doInBackGround():被放到了asynctask的线程池里执行
- onProgressUpdate()和onPostExecute()这两个都是在handler的回调handleMessage里被调用的,所以关键就在handler里的Looper是哪个线程的,他们俩就在哪个线程执行(注意,这里其实跟handler在哪个线程创建没关系,可以在子线程创建handler而传入主线程的Looper)而我们之前也分析了,现在handler的looper只能是主线程的 ,所以这两个方法也是在主线程执行的
如:

1
2
3
4
5
6
7
8
Handler handler=new Handler(getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
Log.i(TAG, "handleMessage: "+Thread.currentThread());
titleBar.setTitle("hahahah");
return false;
}
});

下面是我的测试例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

public class AsyncTest extends AsyncTask<String,Integer,String> {
public static final String TAG="AsyncTask";

public AsyncTest(){
super();
}
@Override
protected String doInBackground(String... strings) {
Log.i(TAG, "doinbackground: "+Thread.currentThread()+strings);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
publishProgress(2);

return "finish";
}

@Override
protected void onPreExecute() {
Log.i(TAG, "onPreExecute: start");
Log.i(TAG, "onPreExecute: "+Thread.currentThread());
}

@Override
protected void onPostExecute(String s) {
Log.i(TAG, "onPostExecute: "+s);
Log.i(TAG, "onpostexecute: "+Thread.currentThread());

}

@Override
protected void onProgressUpdate(Integer... values) {
Log.i(TAG, "onProgressUpdate: "+values[0]);
Log.i(TAG, "onprogressupdate: "+Thread.currentThread());

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Activity里:

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.acitivty_view_test);

new Thread(new Runnable() {
@Override
public void run() {
AsyncTest test=new AsyncTest();
test.execute("aaa");
}
}).start();

}

结果:
asynctask.png
所以,asynctask也不一定非得在主线程创建和执行

最后一问:为啥一个任务只能执行一次?

当我第一次看到这个规则时,我也很奇怪,好不容易创建了,就执行一次就完了?我不信,结果看一下源码确实发现了那个异常,如果是同一个任务就会抛出“通一个任务只能执行一次”,那意义何在呢?
在仔细看一下源码发现他的线程池都是静态的,所以这样更好诠释了asynctask是一个轻量的线程框架的含义,每次没给他其实就代表一次任务,提交给统一的线程池去管理运行。

1
2


为什么 AsyncTask 的对象只能被调用一次,否则会出错?(每个状态只能执行一次)
从上面我们知道,AsyncTask 有 3 个状态,分别为 PENDING、RUNNING、FINSHED,而且每个状态在 AsyncTask 的生命周期中有且只执行一次。由于在执行完 execute 方法的时候会先对 AsyncTask 的状态进行判断,如果是 PENDING(等待中)的状态,就会往下执行并将 AsyncTask 的状态设置为 RUNNING(运行中)的状态;否则会抛出错误。AsyncTask finish 的时候,AsyncTask 的状态会被设置为 FINSHED 状态。

if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException(“Cannot execute task:”
+ “ the task is already running.”);
case FINISHED:
throw new IllegalStateException(“Cannot execute task:”
+ “ the task has already been executed “
+ “(a task can be executed only once)”);
}
}

算法之红黑二叉树

红黑二叉树

1.红黑树的产生

在网上查找的各种关于红黑树的博客,一般就直接把红黑树的五个性质摆上,直接开始介绍,但并不说为什么要使用遵循这些规则性质,为什么会产生他。最近在看《算法》正好看到了红黑二叉树,在此总结一下。
在了解二叉树前必须先提一下2-3树
2节点
3节点
2-3树就是既有2节点又有3节点的树。在我们使用二叉查找树时,不能保证其总在最优情况,也就是保证其为一个平衡的二叉树,只有这时他的查找时间复杂度才是nlogn, 而他在最坏情况下,很有可能事件复杂度变成n,也就是变成了链表。如下情况:
最坏情况
所以一般的二叉查找树并不稳定,于是就出现了2-3树。2-3树的插入分许多不同的情况,十分复杂。也正是因为2-3树插入时所做的各种规定改变,使得2-3树最终是会自平衡的,从而达到平衡二叉树的目的。

实现这些不仅需要大量的代码,而却它们所产生的而外开销会可能使算法比标准的二叉查找树更慢。平衡一棵树的初衷是为了消除最坏情况,单我们希望这种保障所需要的代码能够越少越好。于是,就有了红黑树

所以红黑树是2-3树的变形,以2-3树的角度去理解红黑树就容易多了

2.介绍

红黑二叉树背后的基本思想时用标准的二叉查找树(全是2节点)和一些额外的信息(用红黑节点来代替3节点)来表示2-3树。实现:

  • 红链接将两个2节点连起来构成3节点
  • 黑链接则是2-3树中的普通链接
    (有些地方称之为红黑节点,将3节点的第一个元素,作为第二个元素的左节点,并用红色的线连接,此时红色线连接的节点就相当于红色。

于是红黑二叉树和2-3树的等价的定义:

  • 红链接均为左链接
  • 没有任何一个节点同时和两条红链接相连
  • 该树是完美黑色平衡的,即任意空连接到根节点的路径上黑节点的数量相同
    红黑树与2-3树转换
    满足这些条件的红黑树就和2-3树一一对应:如果将红链接画平,那么所有空连接到根节点的路径相同(高度相同),将红链接合并,得到的就是一颗2-3树。这样既保证了二叉树的平衡,又能使用普通二叉树的查找方法,代价很小。

    变换修复方式:

    这两种变换都是局部性的,不会影响到整棵树的黑色平衡性
    其实这些变换都是与2-3树里的插入时需要的变换是一一对应的,只不过红黑二叉树用红黑链将其简化,所以要是不知道这些变换的原因,就请先去学习2-3树的插入变换

    1.旋转

    在实现某些操作时很可能出现红色有链接后者两条连续的红链接,或两条连续的红链接。所以我们需要对这种情况进行修复:
  • 左旋转: 需要将一条红色有链接转换为左链接,
  • 右旋转:相反
    旋转

    2.颜色转换:

  • 将两个子节点的颜色变黑,同时将父节点的颜色由黑变红。注意当根节点为红色时我们需要把他变黑,(也就是说根节点总是黑色)这是树的黑链接高度+1(原因参考2-3树的建立)

    3. 操作

    因为这些变换都是局部的,所以我们只需递归的执行即可,只需执行左旋转,右旋转和颜色变化呢这三种简单操作,即可保证插入后的红黑树和2-3树一一对应,沿着插入点到根节点的路径向上移动时所经过的每个节点中顺序完成以下操作:
  1. 如果右节点是红色而作节点是黑色,则进行左旋转;
  2. 如果左节点是红色的且它的左子节点也是红色,进行右旋转
  3. 如果左右节点均为红色,则进行颜色变化

仅仅上面三个简单操作就可已概括复杂的2-3树插入时的各种情况

最后

  • 上述方法的代码实现均可在《算法》一书中找到,大部分结论方法也均出自此书中,有兴趣的可自行参考
  • 一篇很好的讲解2-3树的实现的博客从2-3树理解红黑树

Android自定义view-onMeasure

Andorid view 的onMeasure():

1
2
3
4
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
}

1. 理解MeasureSpec

onMeasure方法参数可以看到两个int值,那么怎么通过他来获取测量信息呢?
其实MeasureSpec是一view的内部类,其封装了view的规格尺寸。他有一个很重要的常量,就是一个32位的int 值,很巧妙,其中高2位代表了SpecMode,低30位代表了SpecSIze,(从其源码可以看出)

  • SpecMode 指测量模式:
    • UNSPECIFIED:未指定模式未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量
    • AT_MOST:最大模式,对应于wrap_comtent属性,子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值。
    • EXACTLY:精确模式,对应于 match_parent 属性和具体的数值,父容器测量出 View所需要的大小,也就是SpecSize的值

所以一般自定义view需要重写该方法,给出当测量模式为AT_MOST时的默认宽高大小,否则,当设置其宽高为wrap_content时会和match_parent一样

2.view的Measure流程:

measure 用来测量 View 的宽和高,它的流程分为 View 的 measure 流程和 ViewGroup 的measure流程,只不过ViewGroup的measure流程除了要完成自己的测量,还要遍历地调用子元素的measure()方法。
在自定义view的时候,一般需要处理padding和子元素的margin两种情况

3.两种获取view的宽高方法对比:

  • getMeasuredWidth()

    1
    2
    3
    public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
  • getHeight()

    1
    2
    3
    public final int getHeight() {
    return mBottom - mTop;
    }

    所以从其源码可以看出不同,第一个需要在其onMeasure()方法调用后才能获取到正确的宽高,而第二个需要在onLayout()调用后才能获取到

Android自定义view构造方法

Android自定义view

1. 自定义view的四个构造方法:

  • 第一个public View(Context context)
    这是在activity或其他中直接创建 new时,调用该构造方法,只需要传入context,所以他的属性可以根据提供的各种set***()方法去设置
  • 第二个public View(Context context, AttributeSet attrs)
    这是时当声明在xml文件中,系统在解析这个XML布局的时候调用,可以从attrs这个数组中获取在xml中声明的各种属性,
  • 第三个 public View(Context context, AttributeSet attrs, int defStyleAttrs)
    这个比第二个多出了一个defStyleAttrs,顾名思义,这是一个style,获取当前主题中声明的这个自定义view的style,所以,同个它可以实现不同主题有不同的显示效果。
    这个构造函数通常是通过第二个构造函数调用,如:
    1
    2
    3
    4
    5
    //注意,调用的是this()也就是你写的第三个构造函数 , 而不是super(),否则就不会调用你自己的第三个构造函数了

    public CircleView(Context context, @Nullable AttributeSet attrs) {
    this(context,attrs, R.attr.SimpleCircleStyle);
    }

同时通过其注释:

@param defStyleAttr An attribute in the current theme that contains a reference to a style resource that supplies default values for the view. Can be 0 to not look for defaults.

可看出,要想不让他通过主题里的这个view的style获取默认值,只需把他设置 defStyleAttr=0即可。
再进一步,其实第二个构造函数的实现就是调用了第三个:

1
2
3
4

public View(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
  • 第四个public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)(21)
    这个需要api21,相对于第三个多了defStyleRes,所以这是说明你可以新建一个style,然后用他去代替主题里的这个view的default style,那为什么要这样做呢?
    Theme是全局控制样式的,但是时候我们只想为某几个TextView单独定义样式,那就得使用四个参数的构造函数。
    所以你需要新建一个继承他,然后第二个构造函数调用第三个,defStyleAttr传入0,第三个构造函数再继续调用第四个,defStyleRes传入你自定义的style。

总结:

  • 其实这四个构造函数是相关联的,使用中只会直接调用第一个,或者第二个构造函数;
    第四个通过第三个调用,第三个通过第二个调用。
  • 优先顺序:不管通过哪个获取,都是一个函数:
    obtainStyledAttributes( AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes)
    优先顺序:

    在布局xml中直接定义 >
    在布局xml中通过style定义 >
    自定义View所在的Activity的Theme中指定style引用 >
    构造函数中defStyleRes指定的默认值

使用:

  • 添加自定义属性
    在res/values/目录下增加一个resources xml文件,示例如下(res/values/attrs_my_custom_view.xml):
    1
    2
    3
    4
    <declare-styleable name="CircleView">
    <attr name="circleColor" format="color|reference"/>

    </declare-styleable>
  • 设置在主题中的style:
    需要先声明一个attr:<attr name="SimpleCircleStyle" format="reference"/>
    然后在当前主题中为其赋值:
    <item name="SimpleCircleStyle">@style/SimpleCircleStyle</item>(值为你自定义的一个style)
  • attrs.xml文件中属性类型format值的格式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    "reference" //引用  
    "color" //颜色
    "boolean" //布尔值
    "dimension" //尺寸值
    "float" //浮点值
    "integer" //整型值
    "string" //字符串
    "fraction" //百分数,比如200%

    //枚举类型:
    < attr name="orientation">
    < enum name="horizontal" value="0" />
    < enum name="vertical" value="1" />
    < /attr>

Android view的事件分发机制

Android 自定义view

Android自定义view有如下步骤:

  • 创建view(View子类)
  • 处理view的布局
  • 绘制view
  • 与用户进行交互

android view视图层次

视图层次

view的事件分发机制(与用户进行交互):

MotionEvent

事件分发主要传递的其实就是一系列MotionEvent,看一下他的主要方法:

  • getX()/getY()获取该触摸点相对于当前view的左上角坐标

  • getRawX()/getRawY()获取触摸点相对于整个手机屏幕的坐标

  • getAction()获取事件类型

  • 这里注意一下,其实,Android里有两套坐标系:

    • Android坐标系:以屏幕左上角为顶点getRawY(),getRawX()

    • view坐标系 :以view左上角为起点,getX(),getY()其实他是一个概念,更好的帮助我们开发
      坐标系

事件分发就是将一个MotionEvent传递给一个具体的view。
事件传递的主要过程:
activity—>
window—->
viewgroup(view)

事件分发三大方法:

boolean dispatchTouchEvent(MotionEvent ev)

  • 事件分发的主要方法

  • 如果事件能够传递给当前view,那么他的dispatchTouchEvent方法一定会被调用

  • 伪代码描述:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public boolean onDispatchTouchEvent(MotionEvent ev){
    boolean result=false;
    if(onInterceptTouchEvent(ev)){
    //也就是调用了view的方法,viewgroup继承自view
    result=onTouchEvent;
    }else{
    result=child.dispatchTouchEvent(ev)

    }
    return result;
    }

    优先级顺序

(view)的dispatchTouchEvent—>
onTouch()—->
onTouchEvent()—–>
onclick()

boolean onInterceptTouchEvent(MotionEvent ev)
表示是否拦截此次事件 ,默认返回false
boolean onTouchEvent(MotionEvent ev)
处理触摸事件的主要方法,(如果设置有onTouchListener设置了onTouch,则会调用onTouch,且返回true则不会调用onTouchEvent),里面调用了onClick,onLongClick,(且只要LONG_CLICKABLE或者CLICKABLE这两个任意一个为true,就会返回true),即便是不可用状态

dispatchTouchEvent主要流程(一次viewGroup到view的传递过程,其他的也是这样,递归着来):

  1. (viewGroup)判断是否拦截,如果拦截 (onterceptTouchEvent返回true),则接下来的事件序列都有其来处理(调用他的onTouchEvent),否则进行下一步
  2. 遍历viewGroup里所有的子view(viewGroup),找到符合的view(在点击坐标是否在view内,且view是否在播放动画),如果找到,则调用他的dispatch方法,即完成一次传递(通过dispatchTransformedTouchEvent()
  3. 如果遍历所有子元素后事件都没有被处理,则包含两种情况:
    • viewgroup没有合适子元素
    • 子元素处理了点击事件,但dispatchTouchEvent返回了false(一般是onTouchEvent返回了false)

则继续交由该viewgroup处理

总结一下:

  1. 一但viewGroup拦截了这个事件,则这一整个序列都将交由他来处理,并且不会再次调用onInterceptTouchEvent()方法。所以如果viewGroup想要处理事件,就一是拦截了事件,二是没有找到合适的子view,从而转去调用它自己的onTouchEvent。
  2. view一旦开始处理事件,如果他不消耗ACTION_DOWN,则其他事件也不会交由他处理,
    view如果不消耗除了ACTION_DOWN以外的其他事件,则这事件会消失,最后将传到activity处理
  3. 注意onclick的调用时机是view接收到了ACTION_DOWN和ACTION_UP这两个事件。
  4. onInterceptTouchEvent的调用:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // ViewGroup

    if (actionMasked == MotionEvent.ACTION_DOWN
    || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
    intercepted = onInterceptTouchEvent(ev);
    ev.setAction(action); // restore action in case it was changed
    } else {
    intercepted = false;
    }
    } else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
    }
  • 也就是说如果被拦截,那么mfirstTouchTarget就为null,则跳出去,就不会再次执行onInterceptTouchEvent,所以一定注意这个方法只会执行一次。
  1. disallowIntercept 而且从上面一段源码也可看到有一个特殊的标记量disallowIntercept,这个变量可以由requestDisallowInterceptTouchEvent()设置,他可以不允许viewgroup拦截事件,所以,如果disallowIntercept==true,就直接跳过,不执行 onInterceptTouchEvent(ev),他的作用是可以由子view去调用getParent().requestDisallowInterceptTouchEvent(true);从而处理滑动冲突

  2. 总的来说,view的事件传递就是利用了责任链设计模式,从activity开始忘viewGroup,再往下级view传递,(从DOWN开始,down事件确定了该view是否要消费他)如果能执行,能消费,则该view返回true,表示交由他来处理,并且停止向下传递;而如果返回了false则交由上级view来处理,而上级viewgroup可以拦截下事件,从而将接下来的事件交由他来处理。

Android线程通信机制

Android中的线程通信

前言:

Android开发中我们经常遇到需要切换线程的场景,当进行网络请求,数据库查询(在google新出的jetpack room数据库框架中,强制数据库操作都在新线程上,如果在主线程会抛出异常),文件读写等耗时操作都要新建线程,然后在将结果返回到主线程上。或者想在另一个线程上运行一段代码,这些都是线程间的通信。

但不管你使用何种方式实现:直接使用handle传递信息,使用asynctask,甚至使用rxjava的subscribeon() ,其底层都是通过Android 的handle机制来实现的所以接下来就来探索这一实现过程。

线程共享变量:

刚学过操作系统的我们知道:在同一进程中线程和线程之间资源是共享的,也就是对于任何变量在任何线程都是可以访问和修改的(注意这是在操作系统层面,因为所以线程共用一个进程的资源,但在语言层面,会有局部变量,全局变量的约束,而原则上只要你能找到变量的地址,你就能在任何线程上使用)。
操作系统进程模型

这样一来就好实现了,只需访问所需传递信息的线程的handle就行了,调用handle.send。Android将线程间传递的信息封装成了一个Message类,数据可以传递给Message的object(或复杂一点的bundle字段),调用post()传递代码段其实被封装成了Message的Runnable callback(把代码块当成对象传递)。

线程接收信息

了解了Android线程如何向另一个线程传递信息,那接收信息的线程如何处理呢?,另一个主角looper就要登场了。

1
2
3
4
5
6
7
8
9
10
 (Looper源码)
...
Looper.prepar()
android.os.Handler handler = new Handler(){
@Override
public void handleMessage(final Message msg) {
//这里接受并处理消息
}
};
Looper.loop()

调用这段代码将一个普通线程变成looper线程,looper会不断从其Messagequeue中获取Message传递给这个handle处理(Looper.loop()这个方法其实是个死循环),这个Messagequeue其实是一个阻塞队列,队列中没有东西时会阻塞当前线程。

1
2
3
4
5
6
7
8
looper.loop()主要代码 
for (;;) {
...
Message msg = queue.next();
...
msg.target.dispatchMessage(msg);
...
}

线程本地变量

那么如何根据不同的thread获取其自己的looper呢?这就要用到ThreadLocal了。
ThreadLocal,顾名思义,就是线程的本地变量,他将变量和线程联系起来,是不同线程获取不同的值。其实这个实现很简单一点也不神秘,只是做了一些封装,用起来很方便,用在looper上
(至于如何获取当前运行的线程,其实底部是通过native C语言方法来实现的)

1
2
3
4
5
6
7
8
9
10
 (Looper源码)
...
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
...

想获取只需
sThreadLocal.get()(将本身ThreadLocal<Looper>作为map的键)

Thread里面有一个map:
ThreadLocal.ThreadLocalMap threadLocals

这样就可以实现线程间的通信了!
而rxjava的各种操作符,asyncTask底部都是通过这个来实现的!
另外就不得不提到kotlin的协程了

协程:就是一个线程框架,就是 Kotlin 提供的一套线程封装的 API——扔物线

kotlin的协成比rxjava等其他封装的更好,功能更强大,更方便,推荐大家去学习!

Android超长图加载与subsampling scale image view实现分析

Android中的图片加载一直是很重要的一块,也是很令人头疼的一块,动不动就出现OOM。所以我们有fresco等优秀的第三方框架,什么三级缓存,一行代码就帮我们轻松实现。但当面对超级长超级大分辨率尺寸的图时,就显得无能为力了,如果直接加载到内存中就又会出现OOM。

1.BitmapRegionDecoder

实现长图大图的加载,最关键的类就是BitmapRegionDecoder他可以实现对图片的局部加载。

1
2
3
4
5
6
mDecoder=BitmapRegionDecoder.newInstance(image,false);

//这里不能用option来获取图片的宽和高,因为经过BitmapRegionDecoder类处理过后的inputstream不能在获取的其信息,自动返回-1
imageHeight=mDecoder.getHeight();
imageWidth=mDecoder.getWidth();
bmp = mDecoder.decodeRegion(mRect, option);

这里注意经过BitmapRegionDecoder类处理过后的inputstream不能再用option获取其信息,会自动返回-1。当创建decoder对象后其实并没有将图片加载到内存中,只有调用了bmp = mDecoder.decodeRegion(mRect, option);之后才将这个mrect矩形的图片局部加载到内存中。

自己实现

那么既然Android有这么方便的类,那我们岂不是很简单就可以自己实现啦!所以参考 鸿洋_的Android 高清加载巨图方案 拒绝压缩图片
这篇博客,我们可以自己实现一个简易的加载长图框架:

  • 自定义一个view,重写他的ondraw()onTouchEvent()
  • 创建GestureDetector.OnGestureListener的实现类和scroller去接管触摸事件,在move时记录滑动距离,重写computeScroll()去辅助滑动
  • 初始化我们的局部加载类BitmapRegionDecoder,当屏幕滑动到哪,记录其坐标到rect里然后直接mDecoder.decodeRegion(mRect, option);调用 invalidate()去ondraw()
  • 同时注意设置option.inBitmap开启图片的复用,进一步减少内存占用
  • 另一个减小内存开销的就是设置合适的采样率,根据控件的大小对图片进行合适的采样压缩
    1
    2
    3
    4
    5
    6
    7
    int insamplesize=1;
    while (imageWidth>1.6*width) {
    imageWidth /= 2;
    insamplesize*=2;
    }
    option.inMutable=true;
    option.inSampleSize=insamplesize;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mDecoder!=null) {
    option.inBitmap=bmp;
    matrix.setScale(scale,scale);
    bmp = mDecoder.decodeRegion(mRect, option);
    canvas.drawBitmap(bmp,matrix,bitmapPaint);
    Log.i("TAG", "onDraw: "+bmp.getByteCount());
    }
    }
    注意:这里记录一个小小的坑:当用matrix进行图片的放大时,一定一定要设置画笔Paint,设置抗锯齿等优化,设与不设的差距真的挺大的
    1
    2
    3
    4
    5
    6
    private Paint bitmapPaint;

    bitmapPaint = new Paint();
    bitmapPaint.setAntiAlias(true);
    bitmapPaint.setFilterBitmap(true);
    bitmapPaint.setDither(true);

    结论

    这样一个简易的图片加载框架就实现了,完美的避免了OOM,因为不用把图片完整的加载到内存中!但是,经过实测,这种方法在加载分辨率比较小的大图时滑动还是挺丝滑的,但一遇到分辨率再大一点的,就会感受到明显的滑动的卡顿,于是我把目光转向了subsampling scale image view这个目前应该是最流行的开源框架

2.subsampling scale imag实现原理

这里以原理实现为主,不想贴很多代码,具体可自己下载阅读github地址

1.ImageSource

subsampling scale image view通过这个类ImageSource去获取图片,所以我们的图片资源都需要通过ImageSource去加载,支持从assets,文件,流中加载,从源码上看他其实就是一个工具类,用于方便加载各个路径的文件

2.fullImageSampleSize

.fullImageSampleSamplSize由private int calculateInSampleSize(float scale)计算出,这个值应该是我们首先应该理解的。
他决定了图片是否需要用BitmapRegionDecoder进行区域加载。如果他的计算结果等于1,则表示这张图的分辨率还不够大,不需要进行切割进行区域加载,所以这种情况下是最简单的,直接将图加载进入,放大缩小,移动,都是通过Matrix来实现的,所以接下来就来说一下Matrix

3.Matrix

matrix,矩阵,很多关于图片的功能都能通过他来做一些十分的变换来实现(可惜当年线代没学好。。。)比如图片我的位移,放缩,旋转等等。subsampling scale image view也用了matrix来实现图片的放缩和位移,主要方法是
matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4); 有两个数组srA,rray和dstArray
dstarray数组决定了图片在屏幕的位置,而大图的移动滑动就是通过他来实现的

4.Tile

private static class Tile这个内部类就是切片类,subsampling scale image view中最重要的一个数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static class Tile {

private Rect sRect;
private int sampleSize;
private Bitmap bitmap;
private boolean loading;
private boolean visible;

// Volatile fields instantiated once then updated before use to reduce GC.
private Rect vRect;
private Rect fileSRect;

}

他的属性也很简单就是用来存储图片的一段切片信息,各种rect和bitmap和一个samplesiz其中需要区分一下各个rect

  • srect和filesrect其实是保存这个切片的原始大小区域,也就调用是mDecoder.decodeRegion(mRect, option)区域加载时传入的rect
  • vRect描述绘制在view画布中的实际位置,也就是说图片放大后的上下滑动就是通过改变这个rect结合matrix.setPolyToP()来实现的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
               protected void onDraw(Canvas canvas) {
    ......
    if (matrix == null) { matrix = new Matrix(); }
    matrix.reset();
    setMatrixArray(srcArray, 0, 0, tile.bitmap.getWidth(), 0, tile.bitmap.getWidth(), tile.bitmap.getHeight(), 0, tile.bitmap.getHeight());
    if (getRequiredRotation() == ORIENTATION_0) {
    setMatrixArray(dstArray, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom, tile.vRect.left,
    }...
    ...

    matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4);
    canvas.drawBitmap(tile.bitmap, matrix, bitmapPaint);

    ......
    }
    5.三个task
    TilesInitTask,TileLoadTask ,BitmapLoadTask
    subsampling scale image view内部又创建了三个继承自AsyncTask的task用来在后台加载decode图从而不阻碍ui主线程,更加流畅。
    所以当fullImageSampleSize==1时,就直接用BitmapLoadTask解码整个图片不需切割,当期大于1时,就需要用TileLoadTask区域解码分割后的图片

Map<Integer, List<Tile>> tileMap

最后介绍这个框架的核心,就是这个map
我们知道,图片放得越大,所需要的像素分辨率就要越高才能匹配,要不然就会很模糊。相反,如果图片缩得很小,就不需要很高的分辨率,多了就浪费了。而Android中就可以根据 option.inSampleSize来对图片进行采样压缩,减小分辨率。
所以,根据这个原理,subsampling scale image view将其根据需要计算出不同的采样率,当做key,然后根据不同的采样率进行切割,生成List<Tile>
放大的时候,subsampling scale image view会选取合适的采样率后获取到List<Tile>然后进行解码,并且,他只会解码显示的部分,也就是til.visiable为true时才会解码。否者将其回收。

综上就是subsampling scale image view的大致实现原理

对比

对比自己实现的和subsampling scale image view,后者在大图的切片方面做得更好只将大图切成若干片,在判断是否可见,如果可见就加载到内存中,否者回收;滑动时只改变矩阵的值进行简单的位移变换,进一步提升了流畅度,而且根据不同放缩比例选择合适的采样率,进一步减少内存占用。自己实现的每滑动一次就要重新解码绘制好几次,所以后者性能更高,值得学习。Android中的图片加载一直是很重要的一块,也是很令人头疼的一块,动不动就出现OOM。所以我们有fresco等优秀的第三方框架,什么三级缓存,一行代码就帮我们轻松实现。但当面对超级长超级大分辨率尺寸的图时,就显得无能为力了,如果直接加载到内存中就又会出现OOM。

1.BitmapRegionDecoder

实现长图大图的加载,最关键的类就是BitmapRegionDecoder他可以实现对图片的局部加载。

1
2
3
4
5
6
mDecoder=BitmapRegionDecoder.newInstance(image,false);

//这里不能用option来获取图片的宽和高,因为经过BitmapRegionDecoder类处理过后的inputstream不能在获取的其信息,自动返回-1
imageHeight=mDecoder.getHeight();
imageWidth=mDecoder.getWidth();
bmp = mDecoder.decodeRegion(mRect, option);

这里注意经过BitmapRegionDecoder类处理过后的inputstream不能再用option获取其信息,会自动返回-1。当创建decoder对象后其实并没有将图片加载到内存中,只有调用了bmp = mDecoder.decodeRegion(mRect, option);之后才将这个mrect矩形的图片局部加载到内存中。

自己实现

那么既然Android有这么方便的类,那我们岂不是很简单就可以自己实现啦!所以参考 鸿洋_的Android 高清加载巨图方案 拒绝压缩图片
这篇博客,我们可以自己实现一个简易的加载长图框架:

  • 自定义一个view,重写他的ondraw()onTouchEvent()
  • 创建GestureDetector.OnGestureListener的实现类和scroller去接管触摸事件,在move时记录滑动距离,重写computeScroll()去辅助滑动
  • 初始化我们的局部加载类BitmapRegionDecoder,当屏幕滑动到哪,记录其坐标到rect里然后直接mDecoder.decodeRegion(mRect, option);调用 invalidate()去ondraw()
  • 同时注意设置option.inBitmap开启图片的复用,进一步减少内存占用
  • 另一个减小内存开销的就是设置合适的采样率,根据控件的大小对图片进行合适的采样压缩
    1
    2
    3
    4
    5
    6
    7
    int insamplesize=1;
    while (imageWidth>1.6*width) {
    imageWidth /= 2;
    insamplesize*=2;
    }
    option.inMutable=true;
    option.inSampleSize=insamplesize;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mDecoder!=null) {
    option.inBitmap=bmp;
    matrix.setScale(scale,scale);
    bmp = mDecoder.decodeRegion(mRect, option);
    canvas.drawBitmap(bmp,matrix,bitmapPaint);
    Log.i("TAG", "onDraw: "+bmp.getByteCount());
    }
    }
    注意:这里记录一个小小的坑:当用matrix进行图片的放大时,一定一定要设置画笔Paint,设置抗锯齿等优化,设与不设的差距真的挺大的
    1
    2
    3
    4
    5
    6
    private Paint bitmapPaint;

    bitmapPaint = new Paint();
    bitmapPaint.setAntiAlias(true);
    bitmapPaint.setFilterBitmap(true);
    bitmapPaint.setDither(true);

    结论

    这样一个简易的图片加载框架就实现了,完美的避免了OOM,因为不用把图片完整的加载到内存中!但是,经过实测,这种方法在加载分辨率比较小的大图时滑动还是挺丝滑的,但一遇到分辨率再大一点的,就会感受到明显的滑动的卡顿,于是我把目光转向了subsampling scale image view这个目前应该是最流行的开源框架

2.subsampling scale imag实现原理

这里以原理实现为主,不想贴很多代码,具体可自己下载阅读github地址

1.ImageSource

subsampling scale image view通过这个类ImageSource去获取图片,所以我们的图片资源都需要通过ImageSource去加载,支持从assets,文件,流中加载,从源码上看他其实就是一个工具类,用于方便加载各个路径的文件

2.fullImageSampleSize

.fullImageSampleSamplSize由private int calculateInSampleSize(float scale)计算出,这个值应该是我们首先应该理解的。
他决定了图片是否需要用BitmapRegionDecoder进行区域加载。如果他的计算结果等于1,则表示这张图的分辨率还不够大,不需要进行切割进行区域加载,所以这种情况下是最简单的,直接将图加载进入,放大缩小,移动,都是通过Matrix来实现的,所以接下来就来说一下Matrix

3.Matrix

matrix,矩阵,很多关于图片的功能都能通过他来做一些十分的变换来实现(可惜当年线代没学好。。。)比如图片我的位移,放缩,旋转等等。subsampling scale image view也用了matrix来实现图片的放缩和位移,主要方法是
matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4); 有两个数组srA,rray和dstArray
dstarray数组决定了图片在屏幕的位置,而大图的移动滑动就是通过他来实现的

4.Tile

private static class Tile这个内部类就是切片类,subsampling scale image view中最重要的一个数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static class Tile {

private Rect sRect;
private int sampleSize;
private Bitmap bitmap;
private boolean loading;
private boolean visible;

// Volatile fields instantiated once then updated before use to reduce GC.
private Rect vRect;
private Rect fileSRect;

}

他的属性也很简单就是用来存储图片的一段切片信息,各种rect和bitmap和一个samplesiz其中需要区分一下各个rect

  • srect和filesrect其实是保存这个切片的原始大小区域,也就调用是mDecoder.decodeRegion(mRect, option)区域加载时传入的rect
  • vRect描述绘制在view画布中的实际位置,也就是说图片放大后的上下滑动就是通过改变这个rect结合matrix.setPolyToP()来实现的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
               protected void onDraw(Canvas canvas) {
    ......
    if (matrix == null) { matrix = new Matrix(); }
    matrix.reset();
    setMatrixArray(srcArray, 0, 0, tile.bitmap.getWidth(), 0, tile.bitmap.getWidth(), tile.bitmap.getHeight(), 0, tile.bitmap.getHeight());
    if (getRequiredRotation() == ORIENTATION_0) {
    setMatrixArray(dstArray, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom, tile.vRect.left,
    }...
    ...

    matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4);
    canvas.drawBitmap(tile.bitmap, matrix, bitmapPaint);

    ......
    }
    5.三个task
    TilesInitTask,TileLoadTask ,BitmapLoadTask
    subsampling scale image view内部又创建了三个继承自AsyncTask的task用来在后台加载decode图从而不阻碍ui主线程,更加流畅。
    所以当fullImageSampleSize==1时,就直接用BitmapLoadTask解码整个图片不需切割,当期大于1时,就需要用TileLoadTask区域解码分割后的图片

Map<Integer, List<Tile>> tileMap

最后介绍这个框架的核心,就是这个map
我们知道,图片放得越大,所需要的像素分辨率就要越高才能匹配,要不然就会很模糊。相反,如果图片缩得很小,就不需要很高的分辨率,多了就浪费了。而Android中就可以根据 option.inSampleSize来对图片进行采样压缩,减小分辨率。
所以,根据这个原理,subsampling scale image view将其根据需要计算出不同的采样率,当做key,然后根据不同的采样率进行切割,生成List<Tile>
放大的时候,subsampling scale image view会选取合适的采样率后获取到List<Tile>然后进行解码,并且,他只会解码显示的部分,也就是til.visiable为true时才会解码。否者将其回收。

综上就是subsampling scale image view的大致实现原理

对比

对比自己实现的和subsampling scale image view,后者在大图的切片方面做得更好只将大图切成若干片,在判断是否可见,如果可见就加载到内存中,否者回收;滑动时只改变矩阵的值进行简单的位移变换,进一步提升了流畅度,而且根据不同放缩比例选择合适的采样率,进一步减少内存占用。自己实现的每滑动一次就要重新解码绘制好几次,所以后者性能更高,值得学习。

Android 网络请求心得

Rxjava+retrofit+okhttp的网络请求框架

之需要注意的地方


1 retrofit和okhttp异步回调的线程问题:

  • okhttp异步调用后,callback运行在子线程
  • retrofit异步调用后,其callback运行在主线程,retrofit就是对okhttp的一系列封装,运用了大量设计模式,其回调结果最后通过handle传到了主线程,这点要注意,尤其在更新ui时
  • rxjava封装后,回调线程可以通过其线程调度来很方便的设置

2 rxjava 的Observable封装的 retrofit call的泛型参数:

  • TypeBody(class类):如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Result{
    private String name;
    private String ID;
    }


    //在相应retrofit接口中
    @GET(.....)
    Observable<Result> Login()

    通过添加gson转换器,将网络请求响应中返回的json转换为相应的java类,方便操作,注意这个必须在创建retrofit是添加gson转换器.addConverterFactory(GsonConverterFactory.create())

  • ResponseBody(okhttp包下的)

    1
    2
    @GET(...)
    Observable<ResponseBody> Login()

    这个适用于你不需要将其转换成具体的类,只是想获取响应体,当让你也可以获取body后自己手动解析body数据

  • Response<T>(retrofit包下)
    这个将所有响应结果都封装到了这个response里,他的泛型参数可以填ResponseBody或者class类,通过他不仅可以获取body,还可以获取响应码等。

3 Rxjava的网络请求错误处理

  • 注意,如果是用Response包裹的响应体,那么之前说过,所有东西都被包裹在了Response里,所以即使是错误的响应,如4××/5××,也会包裹在Response里,发送给onNext(),而不会发送给onError(),而错误的响应体可以通过
    1
    2
    3
    if (e instanceof HttpException){
    (HttpException)e).response().errorBody()
    }
    获取错误的响应体。
    而如果不是用Response包裹,那么除了2**的响应吗会进入onNext()里,其他的响应码都会直接进入onError()里,这样就中断了这个观察链

picture_1

参考stackoverfollow

  • 关于rxjava的flatmap链式调用:flatmap可以解决回调地狱,解决连续进行网络请求的问题,如
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    Observable.just("hello")
    .flatMap(new Func1<String, Observable<?>>() {
    @Override
    public Observable<?> call(String s) {
    Log.i(TAG, "call: flatmap -----1");
    s = "hellp flapmap1";
    if (s != null)
    return Observable.error(new NullPointerException("null test"));
    return Observable.just(s);
    }
    }).flatMap(new Func1<Object, Observable<?>>() {


    @Override
    public Observable<?> call(Object o) {
    Log.i(TAG, "call: flatmap-----2");
    return Observable.just("33333");
    }

    }).subscribe(new Subscriber<Object>() {


    @Override
    public void onCompleted() {
    Log.i(TAG, "onCompleted: ");
    }

    @Override
    public void onError(Throwable e) {
    Log.i(TAG, "onError: call");
    if (e instanceof NullPointerException)
    Log.i(TAG, "onError: illegal " + e.getMessage());
    }

    @Override
    public void onNext(Object o) {
    Log.i(TAG, "onNext: ");
    }
    });
    第一个flatmap中抛出了一个异常,那么下一个flatmap就不会调用,直接进入了最后订阅的观察者中,进入onError()里。可见,即便你在flatmap里对上一个被观察者发送的结果进行了操作,但他并不算是一个观察者,只是对其进行了一部分处理,map等操作符也是这样,他们共同组成了一个长长的链,唯一的观察者在最后订阅的Subscriber中处理,所以如果中间任意环节出了异常,那么该链在此中断,直接进入最后观察者的onerror里。
    所以之前的如果用Response包裹,就不会中断观察链,你可以根据响应码做一些处理。

4 okhttp的拦截器及重定向处理

  • okhttp的拦截器是其责任链模式的重要实现,通过一个个拦截器将网络链接的各个部分,模块拆分开,分工明确
  • okhttp中对重定向的处理是通过其第一个内置的拦截器RetryAndFollowUpInterceptor实现的,他通过while循环来不断创建request请求资源,知道响应码为200(或错误),当然他也负责进行失败重试
  • 关于Application interceptors与Network Interceptors区别
    他们其实都是用户自定义的拦截器,只是添加顺序不同,导致有区别 okhttp源码中的添加顺序:
    顺序
    可见Application interceptors第一个添加,而Network Interceptors添加在RetryAndFollowUpInterceptor等拦截器之后,所以Network Interceptors可以拦截到重定向的请求,而Application interceptors只能拦截到刚开始的请求和最后重定向后的结果
    拦截器调用示意图

参考:博客

© 2020 WPY's Android Tour All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero