HgWorts

Think big thoughts , but relish small pleasures.


  • 首页

  • 关于

  • 归档

Java Annotations

发表于 2017-01-15 | 分类于 java | 阅读次数

[TOC]

注解为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便的使用这些数据

也称元数据

定义注解

注解的定义看起来很像接口的定义

1
2
3
4
5
6
public @interface Demo{
String value(); //方法的形式表示属性,没有默认值的话需要手动赋值
String value2() default "helloWorld";
}
// 等价于,本质上是一个集成了Annotation接口的接口
interface Demo extends java.lang.annotation.Annotation{}

元注解:(修饰注解的注解)

  • @Target
    • 用来定义注解应用于什么地方
    • CONSTRUCTOR
    • FIELD
    • LOCAL_VARIABLE
    • METHOD
    • PACKAGE
    • PARAMETER
    • TYPE
  • @Rectetion
    • 定义注解在哪一个级别可用
      • SOURCE
        • 将被编译器丢弃
      • CLASS
        • 注解在class文件中可用,但会被VM丢弃
        • 编译时注解
      • RUNTIME
        • 可使用反射
        • 运行时注解
  • @Document
    • 将此注解包含在JavaDoc中
  • @Inherited
    • 允许子类继承父类中的注释

marker annotation 标注注解

  • 没有元素的注解
1
2
3
@Target(ElementType.Method)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test{}

注解元素

注解元素可用的类型:

  • 所有基本类型

  • String

  • Class

  • enum

  • Annotation

  • 数组

    元素不能有不确定的值,要么具有默认值,要么在使用注解时提供元素的值,且不能以null作为其值

运行时注解和编译时注解

运行时注解

在程序运行时通过反射来获取注解和注解的属性值。但是因为大量使用反射,对程序会有性能上的影响。

调用Class对象、Method对象、Field对象的getAnnotation(Class annotationType)

编译时注解

在代码编译时通过APT(Annotation Processing Tool)获取注解和注解的属性值,并据此生成Java代码。在程序运行时只要调用编译时生成的代码即可,不需要再通过反射去解析注解。比如Dagger、ButterKnife等。

比如使用注解来实现一个简陋版的ButterKnife

例子来自Ryon

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
//Annotation
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
int value() default 0;
}

//Activity
public void autoInjectAllField() {
try {
Class<?> clazz = this.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 查看这个字段是否有我们自定义的注解类标志的
if (field.isAnnotationPresent(ViewInject.class)) {
ViewInject inject = field.getAnnotation(ViewInject.class);
int id = inject.value();
if (id > 0) {
field.setAccessible(true);
field.set(this, this.findViewById(id));//给我们要找的字段设置值
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}

Android中的View

。Android中可以使用IntDef注解来声明常量值(配合BitMask使用更酸爽)

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
public class Toast {
static final String TAG = "Toast";
static final boolean localLOGV = false;

/** @hide */
/*定义部分*/
@IntDef({LENGTH_SHORT, LENGTH_LONG})
@Retention(RetentionPolicy.SOURCE)
public @interface Duration {}

public static final int LENGTH_SHORT = 0;
public static final int LENGTH_LONG = 1;

...

/*作为类型使用时*/
/**
* Set how long to show the view for.
* @see #LENGTH_SHORT
* @see #LENGTH_LONG
*/
public void setDuration(@Duration int duration) {
mDuration = duration;
}

/*做为返回值时*/
/**
* Return the duration.
* @see #setDuration
*/
@Duration
public int getDuration() {
return mDuration;
}
}

Android屏幕相关

发表于 2017-01-15 | 分类于 android | 阅读次数

[TOC]

基础概念

  • 屏幕大小
    • 对角线长度,如5.5寸手机
  • 分辨率
    • 手机屏幕的像素点个数
  • PPI
    • 每英寸像素Pixels Per Inch, 又被称为Dots Per Inch,由对角线的像素点除以屏幕的大小得到

度量单位

  • dp、dip
    • 设备独立像素
  • px、pixels
    • 像素,不同设备显示效果相同
  • pt、point
    • 标准长度单位
  • sp:scaled pixels
    • 放大像素,主要用于字体显示
  • in
    • 英寸
  • mm

转换

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
/**
* This method converts dp unit to equivalent pixels, depending on device density.
*
* @param dp A value in dp (density independent pixels) unit. Which we need to convert into pixels
* @param context Context to get resources and device specific display metrics
* @return A float value to represent px equivalent to dp depending on device density
*/
public static float convertDpToPixel(float dp, Context context){
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
float px = dp * ((float)metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
return px;
}

/**
* This method converts device specific pixels to density independent pixels.
*
* @param px A value in px (pixels) unit. Which we need to convert into db
* @param context Context to get resources and device specific display metrics
* @return A float value to represent dp equivalent to px value
*/
public static float convertPixelsToDp(float px, Context context){
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
float dp = px / ((float)metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
return dp;
}

Android渲染机制

Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染

渲染

UI卡顿解决方法:

  1. 通过Hierarchy Viewer去检测渲染效率,去除不必要的嵌套
  2. 通过show GPU Overdraw去检测Overdraw
    1. getWindow().setBackgroundDrawable(null)
    2. canvas.clipRect
    3. 移除不必要背景
    4. 减少没有必要的layouts、invalidations、overdraw

鸿洋的博客

Activity的组成

  • Activity
    • 基本的页面单元,Activity包含一个Window,window上可以绘制各种view
  • Window
    • 表示顶层窗口,管理界面的显示和事件的相应;每个Activity均会创建一个
    • 当前只有PhoneWindow作为唯一的实现类
    • 独占一个Surface实例的显示区域,每个窗口的Surface由WindowManagerService分配。可以把Surface看做一个画布,应用通过Canvas或OpenGL在上面作画
    • Activity调用attach,创建了Window
  • PhoneWindow
    • Activity和整个View系统交互的接口
    • phoneWindow是把一个FrameLayout进行了一定的包装,并提供了一组通用的窗口操作接口
  • DecorView
    • 是PhoneWindow中的一个内部类
  • ViewRoot
    • 每个DecorView都有一个与之关联的ViewRoot对象,在ActivityThread.handleResumeActivity()方法中建立了它们两者的关联关系。
    • 负责绘制
    • 调用performTraversals进行绘制
  • View
    • 最基本的UI组件,表示屏幕上的一个矩形区域

Activity Constructure

这里面所有View的监听事件,都通过WindowManagerService来进行接收,并通过Activity对象来回调对应的onClickListener

如果调用requestWindowFeature(Window.FEATURE_NO_TITLE),视图树的布局中就仅有Content了,但是requestWindowFeature必须在setContentView之前才能生效

在onCreate()中调用setContentView()后,ActivityManagerService会回调onResume(),此时系统才会把整个DecorView添加到PhoneWindow上,最终完成界面的绘制。(不调用setContentView也会调用onResume)

绘制流程

整个View树的绘图流程是在ViewRoot.java的performTraversals()函数展开的,该函数做的执行过程可简单概括为根据之前设置的状态:

  • 判断是否需要重新计算视图大小(measure)
  • 判断是否需要安置视图的位置(layout)
  • 是否需要重绘(draw)

Android View的绘制流程

measure

为整个View树计算实际的大小,即设置实际的高(mMeasuredHeight和mMeasureWidth),每个View控件的实际宽高都是由父视图和本身视图决定的

Measure

layout

根据子视图的大小以及布局参数将View树放到合适的位置上

onlayout在View的位置发生改变后调用,内容是对子View重新布局

View#onLayout

所以View的onLayout方法默认为空

ViewGroup#onLayout

调用子View的layout

draw

ViewRoot对象的performTraversals()方法调用draw()方法发起绘制该View树,判断DRAWN位,看是否需要重绘

View#draw
  1. 绘制背景
  2. 通过onDraw绘制自身内容
  3. 通过dispatchDraw()绘制子View
  4. 绘制滚动条
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void draw(Canvas canvas) {
. . .
// 绘制背景,只有dirtyOpaque为false时才进行绘制,下同
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}

. . .

// 绘制自身内容
if (!dirtyOpaque) onDraw(canvas);

// 绘制子View
dispatchDraw(canvas);

. . .
// 绘制滚动条等
onDrawForeground(canvas);

}

requestLayout()

调用measure()和layout()

具体细节推荐此文

View的杂谈

发表于 2017-01-10 | 分类于 android | 阅读次数

A view occupies a rectangular area on the screen and is responsible for drawing and event handling

View占据了屏幕上一个矩形区域,并负责他的绘制和事件监听

强烈推荐 中文世界最好的自定义View教程

要点:

  • 矩形区域
  • 绘制和事件监听

绘制流程

view绘制流程

主要是按照以下流程

  • measure
  • layout
  • draw

有一篇详细的文章介绍深入理解Android之View的绘制流程

onMeasure

Called to determine the size requirements for this view and all of its children

  • onMeasure()方法用于测量视图的大小
  • measure()是final的,但是onMeasure()却是可以进行重写的
  • View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure方法。measure()接收两个参数,widthMeasureSpec和heightMeasureSpec
  • 一般调用setMeasuredDimension()
  • RelativeLayout会让子View调用两次onMeasure,LinearLayout在有weight时,也会调用2次,其他情况一次
  • 尽量使用padding代替margin,因为RelativeLayout的子View如果高度和RelativeLayout不同,会引发效率问题。

MeasureSpec

is used by views to tell their parents how they want to be measured and positioned.

MeasureSpecs are used to push requirements down the tree from parent to
child.

MeasureSpec是一个32位int类型
  • 前2位,测量的模式
    • EXACTLY
      • 具体数值
      • match_parent
    • AT_MOST
      • wrap_content
    • UNSPECIFIED
  • 后32位,测量的大小
MeasureSpec = 父容器的规则+View的LayoutParams

如果父View是match_parent,子View是match_parent,则子View的MeasureSpec为AT_MOST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 					    			    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
//getSuggestedMinimumWidth()
result = size;
break;
case MeasureSpec.AT_MOST:
//子View为wrap_content时为父view的size
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

onLayout

Called when this view should assign a size and position to all of its children

  • layout()方法中,首先会调用setFrame()方法来判断视图的大小是否发生过变化,以确定是否有必要对当前的视图进行重绘。如果需要重绘则调用onLayout方法。
  • onLayout()方法View中是空方法,ViewGroup中为抽象方法,每个ViewGroup的子类必须重写这个方法。
  • 一般由ViewGroup重写,调用childView的layout方法

measuredWidth和width的区别

measuredWidth

These dimensions define how big a view wants to be within its parent (see Layout for more details.

测量后的值,是View期望分配的值,是onMeasure中setMeasuredDimension设置的值

在measure()过程结束后就可以得到

width

These dimensions define the actual size of the view on screen, at drawing time and
after layout. These values may, but do not have to, be different from the
measured width and height.

layout之后才能获取到的值,表示画面在屏幕上的真是宽高,未必和measuredWidth相同

比如自定义View的策略就是layout(0,0,50,50),而不管measuredDiemnsion

onSizeChange

Called when the size of this view has changed

  • 此处的size不一定与getMeasuredSize()一致,需要看layout的策略
  • 如果自定义View需要持有mWidth,mHeight,一般在此获得

Draw

六步骤

  1. Draw the background
  2. If necessary, save the canvas’ layers to prepare for fading
  3. Draw view’s content (onDraw)
  4. Draw children
  5. If necessary, draw the fading edges and restore layers
  6. Draw decorations (scrollbars for instance)

onDraw

Called when the view should render its content

一般自定义View在此方法中通过canvas进行绘制

交互事件

事件拦截机制

  • ViewGroup
    • dispatchTouchEvent
    • onInterceptTouchEvent
    • onTouchEvent
  • View
    • diapatchTouchEvent
    • onTouchEvent

onTouchEvent

Called when a touch screen motion event occurs

注意如果在其中生成点击事件,调用onClickListener,最好使用performClick(),保持系统内部一致性

分类

  • ACTION_DOWN
    • ACTION_MOVE,ACTION_UP发生的前提是曾经发生了ACTION_DOWN,如果没有消费ACTION_DOWN,系统不会捕获ACTION_MOVE和ACTION_UP。onTouchEvent的事件回传到父控件 只会发生在ACTION_DOWN中。ACTION_MOVE和ACTION_UP会跟随ACTION_DOWN进行处理
  • ACTION_UP
  • ACTION_MOVE
  • ACTION_CANCEL
    • ACTION_CANCEL是收到事件前驱后,后续事件被父空间拦截的情况下产生的
  • ACTION_OUTSIDE
  • ACTION_CANCEL
  • ACTION_POINTER_DOWN
  • ACTION_POINTER_UP

一些辅助类

TouchSlop

系统能识别出的最小滑动距离

1
ViewConfiguratin.get(this).getScaledTouchSlop();

Velocity Tracker

常用来得到滑动速度
1
2
3
4
5
6
7
8
9
10
11
//init()
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

//int参数是以毫秒为单位的时间
velocityTracker.computeCurrentVelocity(int);
velocityTracker.getXVelocity();

//destroy()
velocityTracker.clear();
velocityTracker.recycle();

GestureDetector

辅助检测用户的手势动作
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
GestureDetector gestureDetector = new GestureDetector(new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return false;
}

@Override
public void onShowPress(MotionEvent e) {

}

@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}

@Override
public void onLongPress(MotionEvent e) {

}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
});

//add this to onTouchEvent
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
}

Region

如果想要监听一个区域,而不是View的矩形区域的点击事件,该怎么办?

传统的setOnClickListener只是监听整个View的矩形区域,想要监听View的特定区域需要使用Region判断点击事件。

比如说下面有一个圆形的自定义View

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
public class RoundImageView extends ImageView {

private Region mClickRegion;

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
mRadius = Math.min(w, h) / 2;

Path clickPath = new Path();
clickPath.addCircle(mRadius, mRadius, mRadius, Path.Direction.CW);
//设定clickRegion,将其与View绑定
mClickRegion.setPath(clickPath, new Region(0, 0, w, h));
}

@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
//判断交互事件是否在Region中
return mClickRegion.contains(x, y) && super.onTouchEvent(event);
}
}

滑动

实现方法

  • scrollTo/By
  • offset()
  • ObjectAnimator
  • 改变MarginLayoutParams
  • layout
  • ObjectAnimatior
  • Scroller
  • ViewDraggerHelper

Scroller

原理是通过不断的computeScroll 不断的重新绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//init()
Scroller mScroller = new Scroller(context);

//invoke
mScroller.startScroll(getScrollX(),getScrollY(),-50,-50);
invalidate();

//系统在draw()方法中调用此方法
//但因为其不会自动调用,所以需要invalidate()>draw()>computeScroll()
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
//true means has not finished
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}

ViewDraggerHelper

  • 基本可以实现不同的滑动、拖放需求
  • 写在ViewGroup中
  • 内部也是使用Scroller来实现的,所以也需重写computeScroll()
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
41
42
43
44
45
46
47
48
49
mViewDraggerHelper = ViewDragHelper.create(this,callback);

@Override
public boolean onInterceptTouchEvent(MotionEvent ev){
return mViewDraggerHelper.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event){
mViewDragHelper.processTouchEvent(event);
return true;
}

@Override
public void computeScroll(){
if(mViewDragHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}

private ViewDragHelper.Callback callback = new ViewDragHelper.Callback(){

@Override
public boolean tryCaptureView(View child , int pointerId){
//指定哪一个子View可以被移动
return child == XXXView;
}

//返回值为0,表示在该方向上不发生滑动
@Override
public int clampViewPositionVertical(View child,int top,int dy){
return top;
}

@Override
public int clampViewPositinoHorizontal(View child,int left,int dx){
return left;
}

//结束拖动后调用
@Override
public void onViewReleased(View releasedChild,float xvel,float yvel){
super.onViewReleased(releasedChild,xvel,yvel);
if(mMainView.getLeft()<500){
mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
ViewCompat.postInvalidateOnAnimation(this);
}
}
}

滑动冲突

  • 父控件进行拦截
    • dispatchTouchEvent()
    • onInterceptTouchEvent()
  • 子控件进行拦截
    • getParent().requestDisallowInterceptTouchEvent(true);
    • 一般在actiondown中判断是否拦截,传入true,在up中传入false取消拦截

四大构造方法

  • View(Context)
    • 代码中构造
  • View(Context,AttributeSet)
    • style默认是使用Context的主题
  • View(Context,AttributeSet,int)
  • View(Context,AttributeSet,int,int)
    • 归根结底XML构造方法都会调用到此方法
    • 这里面有TypedArray的标准读取方法,自定义View的模板可以直接模仿此处

一些杂谈

BitMask

一个有趣的地方,因为View有很多属性,比如clickable、focusable等等,可能高达几十上百个。

这里并没有采用简单的boolean值,而是采用一个全局的int变量和对应的静态BITMASK进行判断,

比如:

1
2
3
4
5
static final int PFLAG_FOCUSED = 0x00000002;
int mPrivateFlags;
public boolean hasFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}

这样只需要极少量的mFlags,和所有View公用的static bitmask,就可以指示出极多的属性值,是一个非常好的思路。

1
2
3
4
public static final int MASK = 0x00000006;
public static final int FLAG_A = 0x00000000;
public static final int FLAG_B = 0x00000002;
public static final int FLAG_C = 0x00000004;

归类

对于聚簇的Listener,封装成一个实体

1
2
3
4
5
6
7
8
9
10
11
static class ListenerInfo {
protected OnCreateContextMenuListener mOnCreateContextMenuListener;

************

private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;

OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
}

ListenerInfo mListenerInfo;

StringBuilder

  1. 指定大小,因为内部是使用char[]存储,默认大小为16,变长时候和ArrayList一样的策略,申请新char[]并逐个复制,能抠一点性能是一点。
  2. 相比String的+操作,虽然编译器有优化,但是在循环语句中还是会生成多个对象

方法中使用临时变量

保持原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Returns the width of the vertical scrollbar.
*
* @return The width in pixels of the vertical scrollbar or 0 if there
* is no vertical scrollbar.
*/
public int getVerticalScrollbarWidth() {
ScrollabilityCache cache = mScrollCache;
if (cache != null) {
ScrollBarDrawable scrollBar = cache.scrollBar;
if (scrollBar != null) {
int size = scrollBar.getSize(true);
if (size <= 0) {
size = cache.scrollBarSize;
}
return size;
}
return 0;
}
return 0
}

padding和margin

Even though a view can define a padding, it does not provide any support for

margins. However, view groups provide such a support.

某些情况下发现使用margin不奏效,不妨试试看使用padding

Android进程与线程

发表于 2017-01-07 | 分类于 android | 阅读次数

围绕Android中的进程与线程做了简要的概述。

按照操作系统中的描述

线程是CPU调度的最小单元,同时线程是一种有限的系统资源

而进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用

一个程序至少有一个进程,一个进程至少有一个线程

操作系统并没有将多个线程看做多个独立的应用,来实现进程间的调度和管理以及资源分配,这就是进程和线程的重要区别

差别

进程是系统进行资源分配和调度的一个独立单位。

线程是进程的一个实体,是CPU调度和分配的基本单位,是比进程更小的能独立运行的基本单位,线程本身不拥有系统资源(除了必不可少的资源如程序计数器、寄存器、栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

主要差别在于是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响,而线程是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的健壮,但是多进程在切换时,资源耗费大,效率要差。

进程

进程在执行过程中拥有独立的内存单元,而多个线程共享内存,极大的提高了程序的运行效率。

进程保活的方式:这篇文章

Foreground process

  • 有一个Activity且它
    • 正在交互
  • 有一个Service且它
    • 绑定到正在交互的Activity
    • “前台运行”,startForeground()
    • 正在执行生命周期回调 onCreate() onStart() onDestroy()
  • 有一个BroadcastReceiver且它
    • 正在执行onReceive()

Visible process

  • 有一个Activity且它
    • 不在交互,但仍可见
  • 有一个Service且它
    • 绑定到可见Activity

Service process

  • 普通Service

故而对于耗时的比如上传等,新建一个Service是比在activity中新建一个线程好得多的。

Background process

  • 所有Activity都对用户不可见

会被保存在LRU列表中,即最近查看的最晚被终止

Empty process

系统有时候会使用空进程做为缓存,以缩短下一次在其中运行组建所需的启动时间。

额外说明

若一个进程A依赖于另一个进程B,则进程B的优先级可能会被提升并保证B的优先级高于A顶优先级。

​ 举例 (A优先级永远高于B):

  • A中的ContentProvider提供数据给B
  • A中的某个service绑定到B的某个组件

进程间通信

IPC InterProcess Communication

RPC Remote Procedure Call

Android默认每个app是一个进程,但也可以通过android:process属性使每个app有多个进程,或者多个app共享某个进程。

有时候通过多进程的方式获取多份内存空间。一般是指定android:process属性,还有一种非常规的方法,通过JNI在native层去fork一个新的进程。

多进程的特性

多进程模式中,不同进程的组件的确会有独立的虚拟机、Application和内存空间。
相当于两个不同的应用采用了SharedUID的模式。

  • 不同的内存空间,数据无法共享
  • 需要谨慎处理代码中的线程同步
  • 需要提防多进程并发导致的文件锁和数据库锁时效的问题

具体问题:

1. 静态成员和单例模式完全失效
2. 线程同步机制失效
3. SharedPreferences可靠性下降
4. Application会多次创建

多进程App适用场景

  • 提高UI进程的稳定性以及各进程各自的稳定性
  • 独立组件充分解耦,充分独立
  • 为用户节约内存,更加灵活(只保留一个非主进程的来满足聊天的推送)
  • 减少引入部分第三方组件所带来的风险
  • 更有效的做UI进程的有损体验(子进程crash后可以重启)
  • 由于独立进程在自己的JVM上,内存方面不会对UI进程的内存分配造成直接的影响,因此在一些内存占用较多如大图预览的时候,可以一次性使用,一次性回收

进程间通信方式

摘自任玉刚的《把玩Android多进程》ppt

Screen Shot 2017-01-07 at 16.37.37

Parcelable和Serializable

Serializable使用简单但是开销很大,序列化和反序列化过程需要大量的IO操作,一般用于将对象序列化到存储设备中或者将对象序列化后通过网络传输。

Parcelable使用麻烦,但效率很高。

Binder

binder是Android中的一种跨进程通信方式

可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder.

从Android Framework角度来说,Binder是ServiceManager连接各种Manager和相应ManagerService的桥梁。

从Android应用层来说,Binder是客户端和服务端进行通信的媒介。当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。

更安全,比如socket的ip地址可以进行伪造,而Binder机制从协议本身就支持对通信双方做身份校验,因而大大提升了安全性,这个也是Android权限模型的基础。

Binder通信模型

如下图,伪装。即代理模式。对代理对象的操作会通过驱动最终转发到Binder本地对象上去完成,当然使用者无需关心这些细节。

Binder伪装

Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,它的引用却遍布于系统的各个进程中。最诱人的是,这个引用和java里引用一样既可以是强类型,也可以是弱类型,而且可以从一个进程传给另一个进程,让大家都能访问同一Server,就像一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。

线程

线程状态转换

Android中的线程

Android系统基于精简过后的linux内核,Linux系统的调度器在分配time slice的时候,采用的CFS(Completely fair scheduler)策略,不仅会参考单个线程的优先级,还会追踪每个线程已经获取到的time slice数量。优先级高的线程不一定能在争取timeslice上有绝对的优势。

Android将进程分为多个group,其中有两种比较重要:

  • default group
    • 能获得绝大部分的timeslice(UI线程就属于此列)
  • background group
    • 工作线程,最多被分配10%的timeslice

其中background group需要开发者显示的归位(官方建议)

1
2
3
4
5
6
7
new Thread(new Runnable(){
@Override
public void run(){
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// do sth
}
}).start();

线程间通信

  1. 共享内存
  2. 文件、数据库
  3. Handler
  4. Java里的wait notify notifyAll
  5. Thead.join
  6. CountDownLatch(等待多个条件达成后)
  7. CyclicBarrier(指定数目的线程都做好准备后一起开始)
  8. 使用Runnable将执行事件传递给Thread
  9. 使用Callable+FutureTask(ExecutorService线程池)

wingjay的博客

锁

ReentrantLock和条件对象

支持重进入的锁,支持一个线程对资源的重复加锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
Lock mLock = new ReentrantLock();
mlock.lock();
try{
//...
}finally{
mlock.unlock();
}

// 条件对象
Condition condition = mLock.newCondition();
// 阻塞当前进程,并放弃锁(收到signalAll后再次获得锁)
condition.await();
condition.signalAll();

Synchronized

Java中每一个对象都有一个锁,且该锁有一个内部条件。

Synchronized相当于对对象锁进行加锁,wait方法将一个线程添加到等待集中,notify和nitifyAll等待线程的阻塞状态,也就是说wait相当于调用condition.await(),notifyAll相当于调用condition.signalAll()

  • 内部类的同步独立于外部类
  • 静态同步方法会锁定它的Class对象

Volatile

为实例域的同步访问提供了免锁的机制,非常简单同时又非常脆弱,某些情况下提供优于锁的性能和伸缩性。

线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。(这是一个抽象概念,并不真实存在,涵盖了缓存、写缓冲区、寄存器等区域)

volatile保证有序性、但不保证原子性。(比如自增)

保证了:

  • 变量的值在使用之前总会从主内存中再读取出来
  • 对变量值的修改总会在完成之后写回到主内存中

使用必须包括两个条件:

  • 对变量的写操作不会依赖于当前值

    • 自增、自减
  • 该变量没有包含在具有其他变量的不变式中

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      public class numRange{
      private volatile int lower, upper;

      public void setLower(int value){
      if(value < upper)
      lower = value;
      }
      // ...
      }

      ​

有时候在并发编程时可以考虑不变模式,让对象的状态是不变的,如果希望修改对象的状态,就会创建对象的副本并将改变写入副本而不改变原来的对象,这样就不会出现状态不一致的情况。比如String就采用了这样的设计。

几种常见的锁

  • 自旋锁
    • 可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环。
  • 阻塞锁
    • 没得到锁的线程等待或者挂起
  • 可重入锁
    • 一个线程可多次获取该锁
  • 悲观锁
    • 每次去拿数据的时候都认为别人会修改,所以会阻塞全部其他线程
  • 乐观锁
    • 每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据

其中lock和sync属于阻塞锁+可重入锁+悲观锁

UI / Main thread

系统启动时创建的线程,用来处理页面的绘制。

不可以把耗时操作放在ui线程中,如网络请求、数据库的读写等等,阻塞超过5s会发生ANR错误。

因为Android UI toolkit不是线程安全的,故而所有的页面绘制都必须放在UI线程中做。

有个黑科技是可以在Activity的onResume()前使用非UI线程绘制UI,因为检测线程是否是UI线程是在ViewRootImpl中进行检测的,而ViewRootImpl是在onResume()时才会进行初始化的

仅限了解,请勿在实际项目中尝试。

在非UI线程中更新UI

简单情况

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable,long)
  • handler
  • AsyncTask

Handler介绍

Screen Shot 2017-01-07 at 17.25.19

主要分为三个部分:

  • Looper
    • 工人,完成MessageQueue里面的任务
    • 用来执行消息队列中的消息,本质是一个while循环,通过pipe机制进行同步
    • 每个Thread最多拥有一个Looper,而Thread只有拥有了Looper,才能初始化Handler
  • MessageQueue
    • 任务队列,采用FIFS
  • Handler
    • 将消息放入MessageQueue

注意点 :

Can’t create handler inside thread that has not called Looper.prepare()

handler所在的线程必须调用过Looper.prepare方法,否则没有looper来进行工作

1
2
3
4
5
6
7
public static final void prepare(){
if(sThreadLocal.get()!=null){
throw new RuntimeException("Only one Looper may be created per thread");
}
//关于ThreadLocal的介绍,放在文章末尾,此处可以理解为一个referfence,可以set和get
sThreadLocal.set(new Looper());
}

所以需要在新开的线程中显示调用Looper.prepare()方法,否则无法在此线程中新建handler(不然sThreadLocal中是取不到looper来进行更新操作的)

Android中有一个现成的类HandlerThread,可以方便使用

1
2
3
4
HandlerThread handlerThread = new HandlerThread("thread_name");
handlerThread.start();
//MyHandler extends android.os.Handler
mHandler = new MyHandler(handlerThread.getLooper());

Thread类

  • 启动了新的线程,没有任务的概念,不能做状态的管理。
  • start之后,run当中的代码就一定会执行到底,中途无法取消。
  • 作为匿名内部类持有了外部类的引用,在线程退出之前,会阻碍GC的回收,在一段时间内造成内存泄露
  • 没有线程切换的接口,要传递处理结果到UI线程,需要些额外的线程切换代码
  • 如果从UI线程启动,该线程优先级默认为Default

AsyncTask

重写方法:

  • onPreExecute
    • 在执行耗时线程前被调用
  • doInBackground(Params…)
    • 在后台线程执行,返回Result,执行过程中调用publicProgress(Progress)来进行任务进度的更新
  • onPostExecute(Result)
    • 在UI线程中执行
  • onProgressUpdate(Progress…)
    • 在UI线程执行,在publishProgress方法调用后执行

在不同的系统版本上串行与并行的执行行为不一致

必须遵守的规则:

  1. Task实例必须在UI线程中创建
  2. execute方法必须在UI线程中调用
  3. 该Task只能被执行一次,多次调用时会出现异常
  4. 不要手动调用onPreExecute….等生命周期方法,使用publishProgress()更新进度

ThreadPoolExecutor

Thread、AsyncTask适合处理单个任务的场景,HandlerThread适合串行处理多任务的场景。当需要并行的处理多任务时,ThreadPoolExecutor是更好的选择。

提供复用机制(线程池)

1
2
3
4
5
6
public static Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

//execute
THREAD_POOL_EXECUTOR.execute(Runnable XX);

代价

  • 每一个新线程至少消耗64kb内存
  • 线程的切换回带来额外开销(switch context)
  • 尽量复用已有的工作线程

ThreadLocal

很多地方出现这个东西,其实它是一个容器,用来存放线程的静态局部变量,保证每一个线程都拥有单独的静态成员变量,保证了线程安全。

ThreadLocal可以近似的认为是Map,它的get方法就是以当前线程为key去map中取对应的T

ThreadLocal为每一个线程提供了一个独立的副本。

Sample

比如说下面这段代码为每个线程创建一个计数器,这时使用不同的线程获得的number就不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Sequence{
int getNumber();
}

public class SequenceByThreadLocal implements Squence{
private static ThreadLocal<Integer> numberContainer = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue(){
return 0;
}
};

public int getNumber(){
numberContainer.set(numberContainer.get()+1);
return numberContainer.get();
}
}

一个简易实现

其实这个数据结构很简单,可以用代码做一个简易的实现

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
public class MyThreadLocal<T>{
private Map<Thread,T> container = Collections.synchronizedMap(new HashMap<Thread,T>());

public void set(T vaule){
container.put(Thread.currentThread(),value);
}

public T get(){
Thread thread = Thread.currentThread();
T value = container.get(thread);
if(value == null && !container.containsKey(key){
valuee = initValue();
container.put(thread,value);
}
return value;
}

public void remove(){
container.put(Thread.currentThread());
}

protected T initialValue(){
return null;
}
}

简单的通讯录实现ContactsList

发表于 2017-01-04 | 分类于 android | 阅读次数

ContactList是仿通讯录制作的一个app demo

主要技术点在RecyclerView,和自定义view

项目地址:https://github.com/hgDendi/ContactsList

界面概览:

ContactsListDemo

ContactsListDemo2

概要

contactsListStructure

如图,主要简单划分为两个部分:

​ 数据源、与界面组件。

​ 数据源主要来自手机的通讯录信息,通过ContentResolver获取。

​ 而界面组件主要有显示列表和侧边栏。而重点在于列表的分组栏的绘制与现实,这就依靠ItemDecoration来进行实现了,这也是难点。

复用方法

FloatingBarItemDecoration传入需要绘制标题栏的position和标题String的map,目前只支持竖项、单列的列表,如需要扩展,请读完此文,明白原理后很容易实现。

IndexBar传入Label的List,通过setListener加入勾子。

FloatingBarItemDecoration

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter’s data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.

ItemDecoration主要是用来对RecyclerView进行一些修饰,是对adapter数据集中的数据视图增加修饰或空位。经常被用来画分割线、强调效果、可见的分组边界等。

getItemOffset()

1
2
3
4
5
6
7
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int position = ((RecyclerView.LayoutParams))
view.getLayoutParams()).getViewAdapterPosition();
outRect.set(0, mList.containsKey(position) ? mTitleHeight : 0, 0, 0);
}

绘制间距,为绘制标题栏空出间隙。主要逻辑是通过当前view的position判断是否需要在上方空出矩形范围。

onDraw()

主要是进行静态标题栏等绘制,即在每组view的上方,即getItemOffset()的区域进行标题栏的绘制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params =
(RecyclerView.LayoutParams) child.getLayoutParams();
int position = params.getViewAdapterPosition();
if (!mList.containsKey(position)) {
continue;
}
drawTitleArea(c, left, right, child, params, position);
}
}

onDrawOver

实现悬浮分组栏,以及悬浮分组栏碰撞效果绘制。

对于整个列表的绘制流程,是遵循如下的顺序:

​ ItemDecoration#onDraw() -> ItemView的绘制 -> ItemDecoration#onDrawOver

故而在onDrawOver中实现可以满足“悬浮”,即在最上层的效果。

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
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
final int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
if (position == RecyclerView.NO_POSITION) {
return;
}
View child = parent.findViewHolderForAdapterPosition(position).itemView;
String initial = getTag(position);
if (initial == null) {
return;
}

//flag指示当标题栏是否发生碰撞(如开头gif图中指示的)
boolean flag = false;
if (getTag(position + 1) != null && !initial.equals(getTag(position + 1))) {
if (child.getHeight() + child.getTop() < mTitleHeight) {
//与restore()对应,表示下面translate平移坐标系只对绘制当前标题栏生效
c.save();
flag = true;
//translate使发生碰撞时,两个标题栏紧贴,制造出挤开的效果(dy<0,表示绘制偏下)
c.translate(0, child.getHeight() + child.getTop() - mTitleHeight);
}
}

c.drawRect(parent.getPaddingLeft(), parent.getPaddingTop(),
parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + mTitleHeight, mBackgroundPaint);
c.drawText(initial, child.getPaddingLeft() + mTextStartMargin,
parent.getPaddingTop() + mTitleHeight - (mTitleHeight - mTextHeight) / 2 - mTextBaselineOffset, mTextPaint);

if (flag) {
c.restore();
}
}

IndexBar

IndexBar是侧边栏的实现,是采用的自定义View的形式。

FontMatrics

在此之前,介绍一个概念FontMatrics,是表征字体的一个矩阵。

定义BaseLine为Text的起始点(类似英文五线谱的baseline)

drawText传入的纵坐标值也为BaseLine所在的纵坐标,而非矩形区域的左下角的纵坐标(这点很重要,否则在开发者模式中开启布局边界会发现字体和边界错乱)

主要有以下几个属性:

  • Top (<0)
    • Ascent可能的最小值(绝对值最大)
  • Ascent (<0)
    • 字体最高处距BaseLine的距离
  • Descent (>0)
    • 字体最低处距BaseLine的距离
  • Bottom (>0)
    • Descent可能的最大值
  • Leading
    • 间距,用于多行文字显示时的距离

fontMatrics

在此例中我们用来计算每个text的高度,以此作为测量View高度的参数。很多时候可以选择不加leanding值, 因为单行多行时候的leading值都为0.(不知道什么时候可以取到非0的值)

1
2
Paint.FontMetrics fm = mPaint.getFontMetrics();
float singleHeight = fm.bottom - fm.top + fm.leading;

onMeasure()

计算View的长宽。

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
41
42
43
44
45
46
47
48
49
50
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}

private int measureWidth(int widthMeasureSpec) {
int result;
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = getSuggestedMinWidth();
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}

//获取建议的最小宽度,尽量保证不会出现显示不下的情况(极端情况下仍会显示不下)
private int getSuggestedMinWidth() {
String maxLengthTag = "";
for (String tag : mNavigators) {
if (maxLengthTag.length() < tag.length()) {
maxLengthTag = tag;
}
}
return (int) (mPaint.measureText(maxLengthTag) + 0.5);
}

private int measureHeight(int heightMeasureSpec) {
int result;
int specMode = MeasureSpec.getMode(heightMeasureSpec);
int specSize = MeasureSpec.getSize(heightMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
Paint.FontMetrics fm = mPaint.getFontMetrics();
float singleHeight = fm.bottom - fm.top + fm.leading;
//这个mLetterSpacingExtra是疏密程度,是自定义属性,默认1.4
mBaseLineHeight = fm.bottom * mLetterSpacingExtra;
result = (int) (mNavigators.size() * singleHeight * mLetterSpacingExtra);
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}

onDraw()

负责绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int height = getHeight();
int width = getWidth();
//高度为0,可能是因为传入参数为空,则不予显示
if (height == 0) {
return;
}
int singleHeight = height / mNavigators.size();

//遍历绘制Text
for (int i = 0; i < mNavigators.size(); i++) {
float xPos = width / 2 - mPaint.measureText(mNavigators.get(i)) / 2;
float yPos = singleHeight * (i + 1);
if (i == mFocusIndex) {
canvas.drawText(mNavigators.get(i), xPos, yPos - mBaseLineHeight, mFocusPaint);
} else {
canvas.drawText(mNavigators.get(i), xPos, yPos - mBaseLineHeight, mPaint);
}
}
}

DispatchTouchEvent()

处理交互事件,主要是监听UP、CANCEL、DOWN、MOVE,其中以DOWN做为起点,CANCEL、UP做为终点,其他为中间状态。以TAG的焦点变更和事件的开始、结束做为重绘的触发点。

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
41
42
43
44
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
final float y = event.getY();
final int formerFocusIndex = mFocusIndex;
final OnTouchingLetterChangeListener listener = mOnTouchingLetterChangeListener;
final int c = calculateOnClickItemNum(y);

switch (event.getAction()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mFocusIndex = -1;
invalidate();
listener.onTouchingEnd(mNavigators.get(c));
break;
case MotionEvent.ACTION_DOWN:
listener.onTouchingStart(mNavigators.get(c));
default:
if (formerFocusIndex != c) {
if (c >= 0 && c < mNavigators.size()) {
listener.onTouchingLetterChanged(mNavigators.get(c));
mFocusIndex = c;
invalidate();
}
}
break;
}
return true;
}

/**
* @param yPos
* @return the corresponding position in list
*/
private int calculateOnClickItemNum(float yPos) {
int result;
//计算当前触摸点属于哪个TAG,超出边界按照边界值返回(尤其在MOVE的时候很容易滑出边界)
result = (int) (yPos / getHeight() * mNavigators.size());
if (result >= mNavigators.size()) {
result = mNavigators.size() - 1;
} else if (result < 0) {
result = 0;
}
return result;
}

ContactsUtils

主要是负责获得缩写,其中英文字符就直接获得英文字符,中文字符通过比对GB2312得到英文缩写

对于中文获得缩写的核心思想如下,是通过比对GB2312值得到中文中声母,继而获得缩写情况。

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
   //GB2312中简体中文的起止,判断范围
private static int BEGIN = 45217;
private static int END = 63486;

/**
* 各声母第一个汉字
* {i、u、v} 不做声母
*/
private static char[] CHAR_TABLE = {'啊', '芭', '擦', '搭', '蛾', '发', '噶', '哈', '击', '喀', '垃','妈', '拿', '哦', '啪', '期', '然', '撒', '塌', '挖', '昔', '压', '匝'};

private static char[] INITIAL_TABLE = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K','L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Y', 'Z'};

//此table是各声母对应的起始GB值,与initialtable对应
private static int[] BORDER = new int[chartable.length + 1];

static {
for (int i = 0; i < CHAR_TABLE.length; i++) {
BORDER[i] = gbValue(CHAR_TABLE[i]);
}
BORDER[CHAR_TABLE.length] = END;
}

//计算char对应的gb值
private static int gbValue(char ch) {
String str = "" + ch;
try {
byte[] bytes = str.getBytes("GB2312");
if (bytes.length < 2) {
return 0;
}
return (bytes[0] << 8 & 0xff00) + (bytes[1] & 0xff);
} catch (Exception e) {
return 0;
}
}

ContactsManager

负责通讯录信息的获取,此处只取了电话号码和联系人名称,使用的是ContentResolver进行查询

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
@NonNull
public static ArrayList<ShareContactsBean> getPhoneContacts(Context mContext) {
ArrayList<ShareContactsBean> result = new ArrayList<>(0);
ContentResolver resolver = mContext.getContentResolver();
Cursor phoneCursor = resolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
new String[]{ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME}, null, null, null);
if (phoneCursor != null) {
while (phoneCursor.moveToNext()) {
String phoneNumber =
phoneCursor.getString(0)
.replace(" ", "")
.replace("-", "");
String contactName = phoneCursor.getString(1);
result.add(new ShareContactsBean(contactName, phoneNumber));
}
phoneCursor.close();
}
//对结果进行排序,这个排序方法写在bean中
Collections.sort(result, new Comparator<ShareContactsBean>() {
@Override
public int compare(ShareContactsBean l, ShareContactsBean r) {
return l.compareTo(r);
}
});
return result;
}

Fragment

发表于 2016-12-30 | 分类于 android | 阅读次数

[TOC]

本质上是view,但会是一个比较重量级的view(具有生命周期)

生命周期

注意点:

Fragment 的生命周期,除了第一次它创建或销毁之外,统统都是由 Activity 所驱动的。

比如说同一个Activity中多个fragment的hide和show,只会调用到onHiddenChange(),并不会调用到onPause的生命周期!(但是用replace的话会重新创建fragment)

系统通过Fragment的setUserVisibleHint(boolean)来控制是否显示,故而若要在ViewPager中实现懒加载,最好将其实现在setUserVisibileHint()中。(ViewPager默认加载相邻的fragment,setOffscreenPageLimit只能设置大于等于1的数字)

下面说很有名的一张生命周期图:

complete_android_fragment_lifecycle

FragmentManager

  • 管理Activity中的所有的fragment
  • 所有fragment被放入一个栈中,每一个fragment都有一个FragmentState实例,相当于snapshot。
  • 内存重启时会把每个Fragment的state存储起来,最终存储到Activity的savedInstanceState中
  • 对于Fragment
    • getFragmentManager,获取父Fragment的FragmentManager(如果已是父Fragment,则会获得Activity的)
    • getChildFragmentManager,获取当前Fragment(可能是嵌套子Fragment)的FragmentManager

FragmentTransaction

1
2
3
4
5
Fragment fragment = new Fragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container,fragment)
.addToBackStack()
.commit();
  • addToBackStack表示将此事务压入栈中,在用户按下back键时会进行回退操作
    • 在上例中表示回到嵌入fragment之前的状态。
  • 执行hide() show(),会调用到onHiddenChanged(),不会调用onDestroyView()等。
  • add() replace()不要在同一阶级的FragmentManager里混搭使用
  • 此commit为异步操作,加入队列。否则请使用commit

与Activity的交互

Activity -> Fragment

  1. 使用argument进行传值

    1
    2
    3
    4
    5
    //in activity
    fragment.setArguments(Bundle);

    //in fragment
    getArgument();
  2. 使用fragmentmanager获取实例

    1
    2
    fragmentManager.findFragmnetById();
    fragmentManager.findFragmentByTag();

Fragment -> Activity

  1. 建立一个callback借口,由Activity来实现。这也是Android模版中的用法。

    也有比较暴力的方法是直接持有Activity引用的,这点一般不会出问题,但是从面向对象角度来说要尽量避免。

    ​

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class DendiFragment extends Fragment{
    private Callbacks mCallbacks;

    public interface Callbacks{
    public void onItemClicked(Integer id);
    }

    @override
    public void onAttach(Activity activity){
    //TODO
    mCallbacks = (Callbacks)activity;
    }

    @override
    public void onDetach(){
    //TODO
    mCallbacks = null;
    }
    }
  2. getActivity()获得宿主Activity

    需要注意,如果当前Fragment已经onDetach(),则此方法会返回null。

    要避免异步任务中getActivity时返回空指针(同样也要注意如果使用引用可能造成的内存泄漏)

内存重启

系统回收会把Activity的状态保存下来,Activity的FragmentManager负责把Activity中的Fragment保存起来。

内存重启时FragmentManager会把每个Fragment的state存储起来,最终存储到Activity的savedInstanceState中。

保存的状态包括argument,故而getArgument()仍然可以拿到正确的数据。

另外需要注意内存重启情况下需要判断onCreate采取正常流程还是恢复流程(恢复流程的话fragment会储存在FragmentManager中,不需要重新new)

LifeCycle

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
/**
*ActivityA中嵌一个FragmentA,
*如果此时启动ActivityB,且ActivityA因为内存不足或其他原因被回收的时候,
*从B回到A,会回调这样的生命周期:
*/

ActivityB.onPause();

ActivityA.onCreate();
FragmentA.onAttach();
ActivityA.onAttachFragment();
FragmentA.onCreate();
FragmentA.onViewCreated();
ActivityA.onStart();
FragmentA.onStart();
ActivityA.onRestoreInstanceState();
ActivityA.onResume();
FragmentA.onResume();

ActivityB.onStop();
ActivityB.onDestroy();

/**
*ActivityA中嵌一个FragmentA,
*按下home,且ActivityA因为内存不足或其他原因被回收的时候,
*从屏幕回到A,会回调这样的声明周期:
*/

ActivityA.onCreate();
FragmentA.onAttach();
FragmentA.onCreate();
ActivityA.onStart();
FragmentA.onStart();
ActivityA.onResume();
FragmentA.onResume();

Fragment Overlap

当Fragment所在Activity被意外清理掉时,会从栈底向栈顶的顺序恢复fragments,并且全部都是以show()的方式,所以会看到界面重叠。(针对hide和show方式进行Fragment展示的场景,24版本以下在内存重启时没有保存mHidden字段)

也有可能是因为onCreate()中又执行了一遍。

1
2
3
4
5
6
7
8
9
10
@Override
protected void onCreate(Bundle savedInstanceState){
//todo

if(savedInstanceState != null){
//可以使用getFragments()获取栈内所有Fragment
getFragemntManager().beginTransaction().show(XX).hide(XX).commit();
}

}

Fragment State Loss

FragmentManager的每一个操作前(增加、移除、改变生命周期状态),都会调用一个方法来检查状态是否被保存过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 在onSaveInstanceState()
* android.support.v4.app.Fragment#onStop
*/
private void checkStateLoss(){
if (mStateSaved) {
throw new IllegalStateException(
"Can not perform this action after onSaveInstanceState");
}
if (mNoTransactionsBecause != null) {
throw new IllegalStateException(
"Can not perform this action inside of " + mNoTransactionsBecause);

}

如果是使用transaction的过程中报错,建议将事务放到onResumeFragment或者onPostResume中执行。

Extra

OnActivityResult

Fragment的onActivityResult只能接受fragment.startActivityForResult,同样的,fragment.startActivityForResult()返回的事件不会被该framgnet所属的activity的onActivityResult()接收到

Remove出栈

加入回退栈,不能使用remove,需要使用popBackStack()进行出栈

Activity

发表于 2016-12-30 | 分类于 android | 阅读次数

[TOC]

An activity is a single, focused thing that the user can do.

生命周期

complete_android_fragment_lifecycle

启动Activity的请求会由Instruction来处理,然后通过Binder向AMS发送请求,AMS内部维护着一个ActivityTask并负责栈内的Activity的状态同步,AMS通过ActivityThread去同步Activity的状态从而完成生命周期的调用。

比如说onCreate和onStart是由方法performLaunchActivity()调用。

onResume是由方法handleResumeActivity调用。

系统资源的释放

一般将系统资源的释放放在onPause中,比如Camera、Sensor、Receivers

但是因为onPause是切换前台Activity必须经历的过程,必须旧Activity执行onPause之后,新Activity才能呈现在屏幕上(即使是用singleTop的启动模式,启动相同的Activity,也会执行onPause(),onNewIntent(),onResume()),所以不能在onPause执行耗时的操作。

启动模式

Activity启动模式

  • launchMode

    • standard
      • 注意并不是在默认包名的任务栈,是出现在启动它时候的任务栈。比如TaskB中的Activity启动了它,它会继续停留在TaskB中
    • singleTop
    • singleTask

      • 检查当前栈中是否存在需要启动的Activity, 清理栈顶直到此Activity位于栈顶

      • 很有趣的一点,应该网上都没有讲到,栈里分别有ABC三个Activity,A为栈底且为singleTask启动模式,C启动A,则会先调用B的onDestroy,然后再调用C的onPause,启动A,销毁C

        1
        2
        3
        4
        5
        6
        7
        05-14 22:35:10.961 12758-12758/com.hgdendi.test D/Main2Activity: ====onDestroy: 
        05-14 22:35:10.970 12758-12758/com.hgdendi.test D/Main3Activity: ====onPause:
        05-14 22:35:10.980 12758-12758/com.hgdendi.test D/MainActivity: onNewIntent:
        05-14 22:35:10.983 12758-12758/com.hgdendi.test D/MainActivity: onResume:
        05-14 22:35:11.017 12758-12793/com.hgdendi.test D/OpenGLRenderer: endAllActiveAnimators on 0x7cfc1fec00 (RippleDrawable) with handle 0x7cfc0f1d40
        05-14 22:35:11.288 12758-12758/com.hgdendi.test D/Main3Activity: ====onStop:
        05-14 22:35:11.288 12758-12758/com.hgdendi.test D/Main3Activity: ====onDestroy:
      • 可以与TaskAffinity配合使用,而TaskAffinity也主要与singleTask和allowTaskReparenting配合使用

    • singleInstance
      • 会出现在一个新的任务栈中,且此任务栈中只存在这一个Activity
      • 此新的任务栈可被多个应用共享
      • 应用于需要与程序分离的界面,如调用紧急呼叫,就是使用这种启动模式
  • clearTaskOnLaunch
    • 只对根Activity生效
    • 每次返回该TASK,都会将该TASK之上的所有其他Activity清除
  • finishOnTaskLaunch
    • 当用户离开这个TASK,再返回的时候,该Activity就会被finish掉
  • alwaysRetainTaskState
    • 只对根Activity生效
    • 所在的TASK栈不接受任何清理命令,一直保持Task状态
  • allowTaskReparenting
    • 在应用退到后台时,能否从启动它的那个Task移动到具有公共affinity的task
    • 比如app启动了web浏览器的一个Activity,这个activity此时在app的task上,当app退到后台再进入前台,会发现这个activity已经到了浏览器应用的task中了

IntentFlag启动模式

  • NEW_TASK
    • Service因为不依附任何栈,所以若从Service中创建新的Activity需要使用此方法
  • SINGLE_TOP
    • 同single_top
  • CLEAR_TOP
    • 同single_task
    • 但是如果不与SINGLE_TOP同时指定,可能会导致启动的Activity被销毁然后再以该Intent进行重建(而不是直接调用onNewIntent生命周期)
  • NO_HISTORY
    • 该Activity启动其他Activity后,就消失了

Android任务栈

Android任务栈被称为一个TASK,一个TASK中的Activity可以来自不同的APP,同一个App的Activity也可能不在一个Task中

  • 不同TASK栈之间不能传递数据,只能由Intent传递

    • 比如调用startActivityForResult启动一个SingleInstance模式的Activity,会立即返回Activity.RESULT_CANCELED

    ​

Intent

显式

隐式

一个Intent只要能匹配任何一组intent-filter即可成功启动对应的Activity

action

  • 存在且必须和任意的一个相同

category

  • 0~多个
  • 必须各个都匹配
  • 在startActivity时系统会加上DEFAULT,故而为了使Activity接收隐式调用,必须指定DEFAULT

Data

要求Intent中必须有data数据,并且能够完全匹配过滤规则中的某一个data

1
2
3
4
5
6
7
<data android:scheme = "string"
android:host = "string"
android:port = "string"
android:path = "string"
android:pathPattern = "string"
android:pathPrefix = "string"
android:mimeType = "string"

主要分为URI + MIMETYPE

Mimetype

指媒体类型,比如image/jpeg、audio/mpeg4-generic和video/*

可以表示图片、文本、视频等不同的媒体格式

URI
://:/[||]
比如content://com.example.project:200/folder/subfolder/etc

Scheme : URI的模式,比如http、file、content。不指定则只支持file和content

Host : URI的主机名,比如www.baidu.com

Port: URI中的端口号,

Path、pathPattern、pathPrefix: 表述路径信息(*前面加两个斜杠,表示单斜杠需要连打四个斜杠)

同时指定Uri和mimetype需要调用setDataAndType方法

1
2
3
4
5
6
7
intent.setDataAndType(Uri.parse("file://abc"),"text/plain");
//获取隐式intent匹配情况
//PackageManager
public abstract List<ResolveInfo> queryIntentActivities(Intent intent,int flags);
//Intent
public abstract ResolveInfo resolveActivity(Intent intent,int flags);
//这里flag使用MATCH_DEFAULT_ONLY标记位,仅仅匹配带有DEFAULT的activity

打开应用市场对App进行评分

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
41
42
43
44
public ArrayList<String> queryInstalledMarketPkgs(Context context) {
ArrayList<String> pkgs = new ArrayList<String>();
if (context == null)
return pkgs;
Intent intent = new Intent();
intent.setAction("android.intent.action.MAIN");
intent.addCategory("android.intent.category.APP_MARKET");
PackageManager pm = context.getPackageManager();
List<ResolveInfo> infos = pm.queryIntentActivities(intent, 0);
if (infos == null || infos.size() == 0)
return pkgs;
int size = infos.size();
for (int i = 0; i < size; i++) {
String pkgName = "";
try {
ActivityInfo activityInfo = infos.get(i).activityInfo;
pkgName = activityInfo.packageName;
} catch (Exception e) {
e.printStackTrace();
}
if (!TextUtils.isEmpty(pkgName))
pkgs.add(pkgName);

}
return pkgs;
}

// 评分
private void tryToGradeApp(){
List<String> packages;
if(mContext==null) return;
packages= Utils.queryInstalledMarketPkgs(mContext);

if(null == packages || 0 == packages.size()){
Toast.makeText(mContext,"Do not find any installed market in your phone.",Toast.LENGTH_SHORT).show();
return;
}

String appName = mContext.getPackageName();
Uri uri = Uri.parse("market://details?id=" + appName);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);

mContext.startActivity(intent);
}

内存重启

出现异常时候,会由Activity调用onSaveInstantceState方法,之后再由window调用顶层布局各个View的onSavedInstantceState方法。

原则 View的记录信息由View记录,Fragment的记录信息由Fragment记录。

  • intent(Activity)、argument(Fragment)中的信息会被自动保存
  • 其他需要持久化保存的信息需要在onSavedInstantceState()中保存
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
// 自定义View的状态存储一般模式

@Override
public Parcelable onSaveInstanceState() {
return new MySavedState(super.onSaveInstanceState(), mFlag);
}

@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof MySavedState)) {
super.onRestoreInstanceState(state);
return;
}
MySavedState myState = (MySavedState) state;
super.onRestoreInstanceState(myState.savedState);
mFlag = myState.flag;
}

private static class MySavedState implements Parcelable {
// 父类的SavedState
Parcelable savedState;
// 本类自己的属性
boolean flag;

MySavedState(Parcelable savedState, boolean flag) {
this.savedState = savedState;
this.flag = flag;
}
}

故而带Fragment的Activity的建议写法为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ActivityWithFragment{
@Override
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.XXX);

Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.contentFrame);
if (fragment == null) {
// Create the fragment
fragment = TasksFragment.newInstance();
ActivityUtils.addFragmentToActivity(
getSupportFragmentManager(), fragment, R.id.contentFrame);
}


// Load previously saved state, if available.
if (savedInstanceState != null) {
TasksFilterType currentFiltering =
(TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY);
mTasksPresenter.setFiltering(currentFiltering);
}
}
}

onSavedInstantceState是在onStop之前启动,但是和onPause没有必然联系。

横竖屏切换

横竖屏切换时Activity的生命周期

1、默认情况下,切屏会重新调用各个生命周期

2、设置Activity的android:configChanges=”orientation|keyboardHidden”时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法(3.2之前还是会调用各个生命周期)

如果横竖屏的界面布局不同,可以在res下新建layout-land和layout-port目录,然后把布局文件扔到这两个目录文件中

Configuration

描述了设备的所有配置信息,会影响到应用程序检索的资源。包括了用户指定的选项(locale和scaling),也包括设备本身的配置(例如input modes,screen size and screen orientation),可以在该类里查看所有影响Configuration Change的属性

常见的引发Configuration Change的属性:

  • 横竖屏切换
    • android:configChanges=”orientation”
  • 键盘可用性
    • android:configChanges=”keyboardHidden”
  • 屏幕大小变化
    • android:configChanges=”screenSize”
  • 语言的更改
    • android:configChanges=”locale”

可能引发的问题

重走生命流程

界面中用户选择了checkbox和radiobutton选项或者通过网络请求显示在界面上的数据在屏幕旋转后Activity被destroy-recreate,这些空间上被选择的状态和界面上的数据都会消失。

进入某个Activity时加载页面进行网络请求,此时旋转屏幕会重新创建网络请求,这样的用户体验非常不好。

伴随异步操作显示一个progressDialog的话,异步任务未完成去旋转屏幕,程序会因为Activity has leaked window而终止。而当old activity被销毁后,线程执行完毕后还是会把结果返回给old activity而非新的activity

建议要override onSaveInstanceState()方法

onConfigChange()

在onConfigChange()中获取不到新的Layout和控件的尺寸位置信息,必须通过消息异步或者延时调用

不可以动态加载xml,可能会有很多硬代码

Activity过渡动画

提供了三种Transition类型

  • 进入
    • 决定Activity中的所有的视图怎么进入屏幕
  • 退出
    • 决定一个Activity中的所有视图怎么退出屏幕
  • 共享元素
    • 决定两个Activities之间的过渡,怎么共享他们的视图

进入、退出

  • explode
    • 从屏幕中间进或出,移动视图
  • slide
    • 滑动,从屏幕边缘进或出,移动视图
  • fade
    • 淡出,通过改变屏幕上的视图的不透明度达到添加或者移除视图

共享元素

  • changeBounds
    • 改变目标视图的布局边界
  • changeClipBounds
    • 裁剪目标视图边界
  • changeTransform
    • 改变目标视图的缩放比例和旋转角度
  • changeImageTransform
    • 改变目标图片的大小和缩放比例

代码

普通动画

1
2
3
4
5
6
7
8
9
10
11
12
//ActivityA
startActivity(intent,ActivityOptions.makeSceneTransitionAnimation(this).toBundle());

//ActivityB先声明后设置
A在代码中声明
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);
B在xml中声明
<item name="android:windowContentTransitions">true</item>

具体设置:
getWindow().setEnterTransition(new Explode());
getWindow().setExitTransition(new Slide());

淡出效果

1
2
3
4
5
6
7
8
9
10
11
12
13
在ActivityA和ActivityB中同时定义
android:transitionName="XXX"

如果只需要一个共享元素,可以在ActivityA中:
startActivity(intent,ActivityOptions.makeSceneTransitionAnimation(this,view,"share").toBundle());

如果需要多个共享元素
startActivity(intent,
ActivityOptions.makeSceneTransitionAnimation(
this,
Pair.Create(view,"share"),Pair.create(fab,"fab")
).toBundle()
);

定义在Theme中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="android:windowAnimationStyle">@style/ActivityAnimTheme</item>
</style>

<style name="ActivityAnimTheme" parent="@android:style/Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/activity_right_in</item>
<item name="android:activityOpenExitAnimation">@anim/activity_right_out</item>
<item name="android:activityCloseEnterAnimation">@anim/activity_right_in</item>
<item name="android:activityCloseExitAnimation">@anim/activity_right_out</item>
</style>

//下面是xml的sample
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromXDelta="100%"
android:fromYDelta="0"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:toXDelta="0%"
android:toYDelta="0" />
</set>
12
hgDendi

hgDendi

17 日志
6 分类
6 标签
GitHub Weibo
© 2017 - 2018 hgDendi
由 Hexo 强力驱动
主题 - NexT.Mist
0%