[Android]Android视图架构详解

转自:http://blog.csdn.net/u012422440/article/details/51173387

最近一直在研究View的绘制相关的机制,发现需要补充一下Android View Architecture的相关知识,所以就特地研究了一下这方面的代码,写成本篇文章
为了节约你的时间,本篇文章内容大致如下:

  • ActivityDecorViewPhoneWindowViewRoot的作用和相关关系

Android View Architecture

先来几张图,大致展现一下Android 视图架构的大概。

感谢网友提醒,泛化和实现这两种关系的箭头画反啦。以后要仔细学一遍UML了,平时经常画,如果有错误可真是闹笑话啊。

Android View Architecture

View各类关系图

View树状图

Activity和Window

总所周知,Activity并不负责视图控制,它只是控制生命周期和处理事件,真正控制视图的是Window。一个Activity包含了一个Window,Window才是真正代表一个窗口,也就是说Activity可以没有Window,那就相当于是Service了。在ActivityThread中也有控制Service的相关函数或许正好印证了这一点。
ActivityWindow的第一次邂逅是在ActivityThread调用Activityattach()函数时。

//[window]:通过PolicyManager创建window,实现callback函数,所以,当window接收到
//外界状态改变时,会调用activity的方法,
final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
    ....
    mWindow = PolicyManager.makeNewWindow(this);
    //当window接收系统发送给它的IO输入事件时,例如键盘和触摸屏事件,就可以转发给相应的Activity
    mWindow.setCallback(this);
    .....
    //设置本地窗口管理器
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    .....
}

attach()中,新建一个Window实例作为自己的成员变量,它的类型为PhoneWindow,这是抽象类Window的一个子类。然后设置mWindowWindowManager

Window,Activity和DecorView

DecorViewFrameLayout的子类,它可以被认为是Android视图树的根节点视图。DecorView作为顶级View,一般情况下它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分(具体情况和Android版本及主体有关),上面的是标题栏,下面的是内容栏。在Activity中通过setContentView所设置的布局文件其实就是被加到内容栏之中的,而内容栏的id是content,在代码中可以通过ViewGroup content = (ViewGroup)findViewById(R.android.id.content)来得到content对应的layout。
Window中有几个视图相关的比较重要的成员变量如下所示:

  • mDecor:DecorView的实例,标示Window内部的顶级视图
  • mContentParent:setContetView所设置的布局文件就加到这个视图中
  • mContentRoot:是DecorView的唯一子视图,内部包含mContentParent,标题栏和状态栏。

Activity中不仅持有一个Window实例,还有一个类型为ViewmDecor实例。这个实例和Window中的mDecor实例有什么关系呢?它又是什么时候被创建的呢?
二者其实指向同一个对象,这个对象是在Activity调用setContentView时创建的。我们都知道ActivitysetContentView实际上是调用了WindowsetContentView方法。

@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) { //[window]如何没有DecorView,那么就新建一个
        installDecor(); //[window]
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    ....
    //[window]第二步,将layout添加到mContentParent
    mLayoutInflater.inflate(layoutResID, mContentParent);
    .....
}

代码很清楚的显示了布局文件的视图是添加到mContentParent中,而且Window通过installDecor来新建DecorView

//[window]创建一个decorView
private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor(); //直接new出一个DecorView返回
        ....
    }
    if (mContentParent == null) {
        //[window] 这一步也是很重要的.
        mContentParent = generateLayout(mDecor); //mContentParent是setContentVIew的关键啊
        .....
    }
    ....
}
protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.
    .......
    //[window] 根据不同的style生成不同的decorview啊
    View in = mLayoutInflater.inflate(layoutResource, null);
    // 加入到deco中,所以应该是其第一个child
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in; //给DecorView的第一个child是mContentView
    // 这是获得所谓的content 
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    }
    .....
    return contentParent;
}

从上述的代码中,我们可以清楚的看到mDecormContentParentmContentRoot的关系。
那么,Activity中的mDecor是何时被赋值的?我们如何确定它和Widnow中的mDecor指向同一个对象呢?我们可以查看ActivityThreadhandleResumeActivity函数,它负责处理Activityresume阶段。在这个函数中,Android直接将Window中的DecorView实例赋值给Activity

final Activity a = r.activity;
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;

Window,DecorView 和 ViewRoot

ViewRoot对应ViewRootImpl类,它是连接WindowManagerServiceDecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot来完成。ViewRoot并不属于View树的一份子。从源码实现上来看,它既非View的子类,也非View的父类,但是,它实现了ViewParent接口,这让它可以作为View的名义上的父视图。RootView继承了Handler类,可以接收事件并分发,Android的所有触屏事件、按键事件、界面刷新等事件都是通过ViewRoot进行分发的。ViewRoot可以被理解为“View树的管理者”——它有一个mView成员变量,它指向的对象和上文中WindowActivitymDecor指向的对象是同一个对象

我们来先看一下ViewRoot的创建过程。由于ViewRoot作为WindowMangerServiceDecorView的纽带,只有在WindowManager将持有DecorViewWindow添加进窗口管理器才创建。我们可以查看WindowMangerGlobal中的addView函数。对WindowManager不太熟悉的同学可以参考《Window和WindowManager解析》

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
        // 创建ViewRootImpl,然后将下述对象添加到列表中
    ....
    root = new ViewRootImpl(view.getContext(), display);

    view.setLayoutParams(wparams);

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    ....
    try {
        // 添加啦!!!!!!!!这是通过ViewRootImpl的setView来完成,这个View就是DecorView实例
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
      ....
    }
    ....
}

那么,Window是什么时候被添加到WindowManager中的呢?我们回到ActivityThreadhandleResumeActivity函数。我们都知道Activity的resume阶段就是要显示到屏幕上的阶段,在Activity也就是DecorView将要显示到屏幕时,系统才会调用addView方法。
我们在handleResumeActivity函数中找到了下面一段代码,它调用了ActivitymakeVisible()函数。

// ActivityThread
r.activity.makeVisible();

//Activity
    //[windows] DecorView正式添加并显示
    void makeVisible() {
        if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);
    }

我们通过源代码发现,正式在makeVisible函数中,系统进行了Window的添加。

引用
http://wiki.jikexueyuan.com/project/deep-android-v1/surface.html
http://blog.csdn.net/guxiao1201/article/details/41744107
http://forlan.iteye.com/blog/2269381

[Android] View的三次measure,两次layout和一次draw

转自:http://blog.csdn.net/u012422440/article/details/52972825

我在《Android视图结构》这篇文章中已经描述了Activity,WindowView在视图架构方面的关系。前天,我突然想到为什么在setContentView中能够调用findViewById函数?View那时不是还没有被加载,测量,布局和绘制啊。然后就搜索了相关的条目,发现findViewById只需要在inflate结束之后就可以。于是,我整理了Activity生命周期和View的生命周期的关系,并再次做一下总结。

为了节约你的时间,本篇文章的主要内容为:
– Activity的生命周期和它包含的View的生命周期的关系
– Activity初始化时View为什么会三次measure,两次layout但只一次draw?
– ViewRoot的初始化过程

Activity的生命周期和View的生命周期

我通过一个简单的demo来验证Activity生命周期方法和View的生命周期方法的调用先后顺序。请看如下截图
截图

onCreate函数中,我们通常都调用setContentView来设置布局文件,此时Android系统就会读取布局文件,但是视图此时并没有加载到Window上,并且也没有进入自己的生命周期。
只有等到Activity进入resume状态时,它所拥有的View才会加载到Window上,并进行测量,布局和绘制。所以我们会发现相关函数的调用顺序是:

  • onResume(Activity)
  • onPostResume(Activity)
  • onAttachedToWindow(View)
  • onMeasure(View)
  • onMeasure(View)
  • onLayout(View)
  • onSizeChanged(View)
  • onMeasure(View)
  • onLayout(View)
  • onDraw(View)大家会发现,为什么onMeasure先调用了两次,然后再调用onLayout函数,最后还有在依次调用onMeasure,onLayoutonDraw函数呢?

ViewGroup的measure

大家应该都知道,有些ViewGroup可能会让自己的子视图测量两次。比如说,父视图先让每个子视图自己测量,使用View.MeasureSpec.UNSPECIFIED,然后在根据每个子视图希望得到的大小不超过父视图的一些限制,就让子视图得到自己希望的大小,否则就用其他尺寸来重新测量子视图。这一类的视图有FrameLayout,RelativeLayout等。
在《Android视图结构》中,我们已经知道Android视图树的根节点是DecorView,而它是FrameLayout的子类,所以就会让其子视图绘制两次,所以onMeasure函数会先被调用两次。

// FrameLayout的onMeasure函数,DecorView的onMeasure会调用这个函数。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    .....
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            ......
        }
    }
    ........
    count = mMatchParentChildren.size();
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            ........
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

你以为到了这里就能解释通View初始化时的三次measure,两次layout却只一次draw吗?那你就太天真了!我们都知道,视图结构中不仅仅是DecorViewFrameLayout,还有其他的需要两次measure子视图的ViewGroup,如果每次都导致子视图两次measure,那效率就太低了。所以Viewmeasure函数中有相关的机制来防止这种情况。


public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  ......
  // 当FLAG_FORCE_LAYOUT位为1时,就是当前视图请求一次布局操作
  //或者当前当前widthSpec和heightSpec不等于上次调用时传入的参数的时候
  //才进行从新绘制。
    if (forceLayout || !matchingSize &&
            (widthMeasureSpec != mOldWidthMeasureSpec ||
                    heightMeasureSpec != mOldHeightMeasureSpec)) {
            ......
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            ......
    }
    ......
}

源码看到这里,我几乎失望的眼泪掉下来!没办法,只能再想其他的方法来分析这个问题。

断点调试大法好

为了分析函数调用的层级关系,我想到了断点调试法。于是,我果断在onMeasureonLayout函数中设置了断点,然后进行调试。

函数调用帧

在《Android视图架构》一文中,我们知道ViewRootDecorView的父视图,虽然它自己并不是一个View,但是它实现了ViewParent的接口,Android正是通过它来实现整个视图系统的初始化工作。而它的performTraversals函数就是视图初始化的关键函数。
对比两次onMeasure被调用时的函数调用帧,我们可以轻易发现ViewRootImplperformTraversals函数中直接和间接的调用了两次performMeasure函数,从而导致了View最开始的两次measure过程。
然后在相同的performTraversals函数中会调用performLayout函数,从而导致View进行一轮layout过程。
但是为什么这次performTraversals并没有触发View的draw过程呢?反而是View又将重新进行一轮measure,layout过程之后才进行draw。

两次performTraversals

通过断点调试,我们发现在View初始化的过程中,系统调用了两次performTraversals函数,第一次performTraversals函数导致了View的前两次的onMeasure函数调用和第一次的onLayout函数调用。后一次的performTraversals函数导致了最后的onMeasure,onLayout,和onDraw函数的调用。但是,第二次performTraversals为什么会被触发呢?我们研究一下其源码就可知道。


private void performTraversals() {
    ......
    boolean newSurface = false;
    //TODO:决定是否让newSurface为true,导致后边是否让performDraw无法被调用,而是重新scheduleTraversals
    if (!hadSurface) {
        if (mSurface.isValid()) {
            // If we are creating a new surface, then we need to
            // completely redraw it.  Also, when we get to the
            // point of drawing it we will hold off and schedule
            // a new traversal instead.  This is so we can tell the
            // window manager about all of the windows being displayed
            // before actually drawing them, so it can display then
            // all at once.
            newSurface = true;
                    .....
        }
    }
            ......
    if (!cancelDraw && !newSurface) {
        if (!skipDraw || mReportNextDraw) {
            ......
            performDraw();
        }
    } else {
        if (viewVisibility == View.VISIBLE) {
            // Try again
            scheduleTraversals();
        } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
            for (int i = 0; i < mPendingTransitions.size(); ++i) {
                mPendingTransitions.get(i).endChangingAnimations();
            }
            mPendingTransitions.clear();
        }
    }
    ......
}

由源代码可以看出,当newSurface为真时,performTraversals函数并不会调用performDraw函数,而是调用scheduleTraversals函数,从而再次调用一次performTraversals函数,从而再次进行一次测量,布局和绘制过程。
我们由断点调试可以轻易看到,第一次performTraversals时的newSurface为真,而第二次时是假。
断点调试截图

总结

虽然我已经通过源码得知View初始化时measure三次,layout两次,draw一次的原因,但是Android系统设计时,为什么要将整个初始化过程设计成这样?我却还没有明白,为什么当Surface为新的时候,要推迟绘制,重新进行一轮初始化,这些可能都要涉及到Surface的相关内容,我之后要继续学习相关内容!

[Andorid]有赞Android客户端网络架构演进

转自:有赞技术团队博客 http://tech.youzan.com/android_http/

Android客户端网络请求是每一个应用都不可或缺的模块,其设计的好坏直接影响应用的性能和代码稳定性、扩展性。Android网络请求最开始官方只提供了最基础的方法,开发者必须在此基础上进行二次封装,这样就要求开发者对Http请求协议、缓存、JSON转换、错误处理以及线程切换等都比较熟悉,稳定性、可扩展性和可维护性都是比较大的挑战。

目前Android主流的网络请求都是基于Square公司开发的OkHttp,该框架也得到了Google官方的认可,OkHttp对网络请求做了大量的封装和优化,极大降低了开发者的使用成本,同时兼备稳定性和可扩展性。目前有赞Android客户端也是采用OkHttp进行网络请求,在OkHttp框架的基础上做了大量的封装和优化,减小业务逻辑与框架的耦合性的同时,也极大降低了业务方的使用成本。

1. 现在的网络请求

先以Get请求为例,代码如下:

private static final String TRADE_CATEGORIES = "kdt/tradecategories/get";  
/**
 * 获取订单类型
 *
 * @param context context
 * @param callback TradeCategoryEntity callback
 */
public void requestTradeCategories(Context context,Map<String, String> params, BaseTaskCallback<List<TradeCategoryEntity>> callback) {  
    RequestApi api = createRequestTokenApi(TRADE_CATEGORIES);
    if (null != params) {
        api.addMultipleRequestParams(params);
    }
    api.setMethod(RequestApi.Method.GET);
    api.setResponseKeys(GoodsTradeApi.ResponseKey.RESPONSE, "categories");
    doRequest(context, api, true, callback, TaskErrorNoticeMode.NONE);
}

针对上述Get请求需要几点说明:

  1. 每个模块都需要一个单独的网络请求类,罗列该模块所有的业务请求方法
  2. 请求url需要在每个请求类中定义,如上文中的TRADE_CATEGORIES
  3. 每次请求都需要调用createRequestTokenApi方法,完成token参数的封装
  4. 请求参数可以直接通过addRequestParams添加,也可以以Map的形式统一添加addMultipleRequestParams
  5. 请求方法通过setMethod方法设置
  6. 返回值可以通过setResponseKeys指定需要的具体JSON节点
  7. 请求结果通过BaseTaskCallback回调,返回值类型也可通过泛型方式指定
  8. 回调方法包括请求成功、请求失败以及请求时机等方法

以下是请求结果回调方法代码:

// 请求开始的回调方法
public void onBefore(RequestApi api) {

}
// 请求结束的回调方法
public void onAfter() {

}
// 请求成功的回调方法,data表示返回值,statusCode表示请求状态值
public void onResponse(T data, int statusCode){

}
// 网络请求失败的回调方法
public void onError(ErrorResponse errorResponse) {

}
// 请求的业务错误回调方法
public void onRequestError(ErrorResponse errorResponse) {

}

业务方只需要在不同的回调方法中处理不同的业务即可,比如说在onBefore中显示进度条,在onAfter中隐藏进度条,在onResponse中获取返回值以及状态值,onRequestError中获取错误类型,以及显示错误提示等等。

针对上述请求过程,可以发现还是有一些不是很合理的地方。

  1. 每次新建一个网络接口方法都需要设置请求url、封装token、设置请求方法等,造成了大量的重复代码
  2. 请求url和具体的请求方法分开定义,不够直观
  3. 如果请求参数比较多的时候,使用Map方式容易写错,而且在编译过程和自测阶段不会有错误提示
  4. 请求成功回调方法的线程切换还是需要借助Handler等方式实现

2. 改进后的网络请求框架

针对以上问题,我们引入Square公司开发的Retrofit和ReactiveX出品的RxJava,以下是结合有赞业务后网络请求用法。

首先定义网络请求方法,Retrofit是通过接口的方式完成请求方法定义的,代码如下:

public interface TradesService {  
    @GET("api.tradecategories/1.0.0/get")
    Observable<Response<CategoryResponse>> tradeCategories();

    @FormUrlEncoded
    @GET("api.trade/1.0.0/get")
    Observable<Response<TradeItemResponse>> tradeDetail(@Field("tid") String tid);
}

通过注解的方式指定请求类型、请求url、请求参数,返回值是RxJava的Observable对象,就可以进行一系列的链式操作,具体用法等一下会详细讲述。通过对比两种请求方法的写法,很显然,通过Retrofit定义请求方法更简洁清晰,下面我们来看一下如何通过RxJava实现错误信息的统一处理。

2.1 请求错误处理

首先定义一个BaseResponse,所有的Response都要继承自它。

public class BaseResponse {  
    public static final int CODE_SUCCESS = 0;

    public String msg;
    public int code;
    @SerializedName("error_response")
    public ErrorResponse errorResponse;

    public static final class ErrorResponse {
        public String msg;
        public int code;
    }
}

BaseResponse定义了错误信息,后续所有的Response都会继承BaseResponse,而服务端返回的错误信息都会进行集中处理,处理逻辑代码如下:

 // 下文的T:T extends Response<R>, R: R extends BaseResponse
 public Observable<R> call(Observable<T> observable) {
    return observable.map(new Func1<T, R>() {
        @Override
        public R call(T t) {
            String msg = null;
            if (!t.isSuccessful() || t.body() == null) {
                msg = mDefaultErrorMsg;
            } else if (t.body().code != BaseResponse.CODE_SUCCESS) {
                msg = t.body().msg;
                if (msg == null) {
                    msg = mDefaultErrorMsg;
                }
                tryLogin(t.body().code);
            } else if (t.body().errorResponse != null) {
                msg = t.body().errorResponse.msg;
                if (msg == null) {
                    msg = mDefaultErrorMsg;
                }
                tryLogin(t.body().errorResponse.code);
            }
            if (msg != null) {
                try {
                    throw new ErrorResponseException(msg);
                } catch (ErrorResponseException e) {
                    throw Exceptions.propagate(e);
                }
            }
            return t.body();
        }
    });     

关于上面的代码有几点需要说明:

  1. T继承了Retrofit提供的Response类,该类包含了Http返回值、协议版本号等等通用信息,R是具体的业务数据结构,如同上文所说,R继承了BaseResponse,方便统一处理错误信息。
  2. 业务的错误返回值都是事先定义好的,只需要根据返回的错误码和错误类型进行处理即可
  3. 这里的错误类型本质上是利用RxJava的map转换方法,即将一种Observable转换成另一种Observable,转换的过程中对不同的错误类型进行处理,同时将处理后的结果通过Observable传递出去

2.2 线程切换

正如上文所言,之前的请求方法在请求回调方法中无法方便的切换线程,必须要借助Android原生的Handler方式,代码就会比较分散,可读性也比较差,而RxJava针对线程切换提供了很友好的处理方法,只需要显式的设置即可完成不同线程的切换。

public class SchedulerTransformer<T> implements Observable.Transformer<T, T> {  
    @Override
    public Observable<T> call(Observable<T> observable) {
        return observable
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }
}

上面的代码主要完成了两部分的工作,subscribeOn声明生产者Observable所在的线程,由于网络请求比较耗时,一般会放到IO线程或者单独的子线程;而observeOn声明了处理请求返回值所在的线程为Android系统的主线程,也即UI线程,这样就可以在处理返回值时更新UI。

2.3 请求日志

网络请求经常会出现各种各样的异常情况,这个时候就需要通过查看请求日志来跟踪定位问题,OkHttp提供了打印完整日志的方法,方便开发者调试网络请求。OkHttp官方提供了一个很方便查看日志的Interceptor,你可以控制你需要的打印信息类型,使用方法也很简单。

首先需要在build.gradle文件中引入logging-interceptor

compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'  

在OkHttpClient创建处添加创建好的Interceptor即可,完整的示例代码如下:

private static OkHttpClient getNewClient(){  
    HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
    logging.setLevel(HttpLoggingInterceptor.Level.BODY);
    return new OkHttpClient.Builder()
           .addInterceptor(logging)
           .build();
}

HttpLoggingInterceptor提供了4中控制打印信息类型的等级,分别是NONE,BASIC,HEADERS,BODY,接下来分别来说一下相应的打印信息类型。

  • NONE

    没有任何日志信息

  • Basic

    打印请求类型,URL,请求体大小,返回值状态以及返回值的大小

  D/HttpLoggingInterceptor$Logger: --> POST /upload HTTP/1.1 (277-byte body)  
  D/HttpLoggingInterceptor$Logger: <-- HTTP/1.1 200 OK (543ms, -1-byte body)  
  • Headers

    打印返回请求和返回值的头部信息,请求类型,URL以及返回值状态码

  <-- 200 OK https://your_url (3787ms)
  D/OkHttp: Date: Sat, 06 Aug 2016 14:26:03 GMT
  D/OkHttp: Content-Type: application/json; charset=utf-8
  D/OkHttp: Transfer-Encoding: chunked
  D/OkHttp: Connection: keep-alive
  D/OkHttp: Keep-Alive: timeout=30
  D/OkHttp: Vary: Accept-Encoding
  D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
  D/OkHttp: Pragma: no-cache
  D/OkHttp: Cache-Control: must-revalidate, no-cache, private
  D/OkHttp: Set-Cookie: bid=D6UtQR5N9I4; Expires=Sun, 06-Aug-17 14:26:03 GMT; Domain=.douban.com; Path=/
  D/OkHttp: X-DOUBAN-NEWBID: D6UtQR5N9I4
  D/OkHttp: X-DAE-Node: dis17
  D/OkHttp: X-DAE-App: book
  D/OkHttp: Server: dae
  D/OkHttp: <-- END HTTP
  • Body

    打印请求和返回值的头部和body信息

  <-- 200 OK https://your_url (3583ms)
  D/OkHttp: Connection: keep-alive
  D/OkHttp: Date: Sat, 06 Aug 2016 14:29:11 GMT
  D/OkHttp: Keep-Alive: timeout=30
  D/OkHttp: Content-Type: application/json; charset=utf-8
  D/OkHttp: Vary: Accept-Encoding
  D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
  D/OkHttp: Transfer-Encoding: chunked
  D/OkHttp: Pragma: no-cache
  D/OkHttp: Connection: keep-alive
  D/OkHttp: Cache-Control: must-revalidate, no-cache, private
  D/OkHttp: Keep-Alive: timeout=30
  D/OkHttp: Set-Cookie: bid=ESnahto1_Os; Expires=Sun, 06-Aug-17 14:29:11 GMT; Domain=.douban.com; Path=/
  D/OkHttp: Vary: Accept-Encoding
  D/OkHttp: X-DOUBAN-NEWBID: ESnahto1_Os
  D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
  D/OkHttp: X-DAE-Node: dis5
  D/OkHttp: Pragma: no-cache
  D/OkHttp: X-DAE-App: book
  D/OkHttp: Cache-Control: must-revalidate, no-cache, private
  D/OkHttp: Server: dae
  D/OkHttp: Set-Cookie: bid=5qefVyUZ3KU; Expires=Sun, 06-Aug-17 14:29:11 GMT; Domain=.douban.com; Path=/
  D/OkHttp: X-DOUBAN-NEWBID: 5qefVyUZ3KU
  D/OkHttp: X-DAE-Node: dis17
  D/OkHttp: X-DAE-App: book
  D/OkHttp: Server: dae
  D/OkHttp: {"count":3,"start":0,"total":778,"books":[{"rating":{"max":10,"numRaters":202900,"average":"9.0","min":0},"subtitle":"","author":["[法] 圣埃克苏佩里"],"pubdate":"2003-8","tags":[{"count":49322,"name":"小王子","title":"小王子"},{"count":41381,"name":"童话","title":"童话"},{"count":19773,"name":"圣埃克苏佩里","title":"圣埃克苏佩里"}
  D/OkHttp: <-- END HTTP (13758-byte body)

2.3 统一添加token和User-Agent

现在基本上所有的应用都会通过token来鉴定用户权限,User-Agent参数方便服务端获取更多手机本地的信息,类似这样的每一个请求都需要的参数,也可以通过Interceptor方式实现。

public class LoginInterceptor implements Interceptor {  
    public static final String ACCESS_TOKEN = "access_token";
    public static final String USER_AGENT = "User-Agent";

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();
        Request request = original.newBuilder()
                .header(USER_AGENT, AppUtils.getUserAgent() + " app_name/" + AppUtils.getVersionName())
                .header(ACCESS_TOKEN, AppUtils.getToken())
                .method(original.method(), original.body())
                .build();
        return chain.proceed(request);
    }
}

2.4 通过对象封装请求体

有的时候post请求的参数会有很多个,如果一个个写,那么方法的参数就显得很多,杂乱且不好维护,当然可以用Map的方式集中管理,但是Map有一个问题就是一旦key值写错了,前期是无法及时发现的,必须等到真正的网络请求的时候才能发现。Retrofit提供了一个很好的解决方法,通过对象封装这些请求参数。

@FormUrlEncoded
@POST("book/reviews")
Observable<Response<String>> addReviews(@Body Reviews reviews);

public class Reviews {  
    public String book;
    public String title;
    public String content;
    public String rating;
}

需要注意的是类中的属性名必须要和请求参数的key保持一致,否则服务端无法正常识别请求参数

关于Retrofit详细的用法可以参考文章Retrofit详解

3. 如何使用

我们以获取Category为例来说明如何利用RetrofitRxAndroid来改写现有模块。

3.1 定义CategoryResponse

CategoryResponse必须继承自BaseResponse,里面包含了错误信息的数据结构。

@Keep
public class CategoryResponse extends BaseResponse {  
    public Response response;

    @Keep
    public static final class Response {
        public List<Category> categories;
    }
}

其中Category是具体的实体类型。

3.2 定义Service Method

public interface TradesService {  
    @GET("api/tradecategories/get")
    Observable<Response<CategoryResponse>> tradeCategories();
}

注意点

  • TradesService必须是一个interface,而且不能继承其他interface
  • tradeCategories的返回值必须是Observable类型。

3.3 利用ServiceFactory创建一个TradeService实例

在适当的时机(Activity#onCreateFragment#onViewCreated等)根据网关类型通过ServiceFactory创建一个TradeService实例。

mTradesService = ServiceFactory.createNewService(TradesService.class)  

3.4 TradeService获取数据

mTradesService.tradeCategories()  
        .compose(new DefaultTransformer<CategoryResponse>(getActivity()))
        .map(new Func1<CategoryResponse, List<Category>>() {
            @Override
            public List<Category> call(CategoryResponse response) {
                return response.response.categories;
            }
        })
        .flatMap(new Func1<List<Category>, Observable<Category>>() {
            @Override
            public Observable<Category> call(List<Category> categories) {
                return Observable.from(categories);
            }
        })
        .subscribe(new ToastSubscriber<Category>(getActivity) {
            @Override
            public void onCompleted() {
                hideProgressBar();
                // business related code
            }

            @Override
            public void onError(Throwable e) {
                super.onError(e);
                hideProgressBar();
                // business related code
            }

            @Override
            public void onNext(Category category) {
                // business related code
            }
        });

注意:DefaultTransformer包含了线程分配错误处理两部分功能,所以调用方只需要关心正确的数据就可以了。

DefaultTransformer包含了上文提到的线程切换转换器SchedulerTransformer和错误处理转换器ErrorCheckerTransformer,具体代码如下:

public class DefaultTransformer<R extends BaseResponse>  
        implements Observable.Transformer<Response<R>, R> {

    private Context context;

    public DefaultTransformer(final Context context) {
        this.context = context;
    }

    @Override
    public Observable<R> call(Observable<Response<R>> observable) {
        return observable
                .compose(new SchedulerTransformer<Response<R>>())
                .compose(new ErrorCheckerTransformer<Response<R>, R>(context));
    }
}

4.写在最后

网络请求对于提升Android应用的体验和性能有很大的影响,结合Retrofit使用提高了代码可读性和编码的灵活性,RxJava提供了链式调用方式,融合了线程切换、数据过滤、数据转换等优点。有赞在此基础上进行了少量的封装,便可适应大部分复杂的业务场景。

[Android] apk打包问题

多次在生产签名打包后的apk,出现功能不可用的情况,比方说有个社会化分享功能,写代码时都可以正常实现,但签名生成apk后该功能无法再使用了,点击分享面板的平台,没有任何响应。请问是怎么回事,这种问题解决应该从哪几个方面入手,希望有一些思路可供参考

  • 应该是混淆引起的
  • 混淆是将易读性较好的变量,方法和类名替换成可读性较差的名称
  • 混淆的目的是为了加大逆向的成本,但不能避免
  • 通常混淆的处理是将某些库不加入混淆
  • 第三方的库不建议混淆

一些需要排除混淆的

  • 被native方法调用的java方法
  • 供javascript调用的java方法
  • 反射调用的方法
  • AndroidManifest中声明的组件
  • 总结:即所有硬编码的元素(变量,方法,类)

关于混淆,请参考文章读懂 Android 中的代码混淆

[Android] Bitmap优化

Bitmap优化

  • options.inJustDecodeBounds = true;可以获取width,height和mimetype等信息,但不会申请内存占用
  • 合理进行缩放,一个高分辨率的图片不仅展示在一个小的imageView中,不仅不会有任何视觉优势,反而还占用了很大的内存
  • 将Bitmap处理移除主线程
  • 使用LruCache或者DiskLruCache缓存Bitmap
  • before 2.3 手动调用recycle()方法

关于Bitmap的文章

[Android] 把Activity作为参数传给一个静态方法,会影响这个Activity的正常销毁吗

把Activity作为参数传给一个静态方法,会影响这个Activity的正常销毁吗

  • 内存泄露与方法是否是静态与否无关,与内部的方法体实现有关系。
  • 内存泄露可以简单理解成:生命周期长的对象不正确持有了持有了生命周期短的对象,导致生命周期短的对象无法回收。
  • 比如Activity实例被Application对象持有,Activity实例被静态变量持有。

关于Android中内存泄漏的文章

[Android] static 单例是怎么保证单例的?

static 单例是怎么保证单例的?没太看明白

  • static变量为类所有
  • staitc只初始化一次,即在调用的时候。
  • 如下代码,STATIC_OBJECT只在第一次调用时初始化,后续调用则不会再执行初始化
1
2
3
public class Example {
    public static Object STATIC_OBJECT = new Object();
}
  • 使用static机制创建单例
1
2
3
4
5
6
7
8
9
10
11
12
public class SingleInstance {
    private SingleInstance() {
    }

    public static SingleInstance getInstance() {
        return SingleInstanceHolder.sInstance;
    }

    private static class SingleInstanceHolder {
        private static SingleInstance sInstance = new SingleInstance();
    }
}

关于单例的文章

[Android] 业务场景:需要定时后台扫描数据库,上传本地照片至云端,定时任务采用何种模式

业务场景:需要定时后台扫描数据库,上传本地照片至云端,定时任务采用何种模式

  • Handler或者Timer定时一般为秒级别的任务,Timer会启动额外线程,而Handler可以不用。
  • 无论是Handler还是Timer都需要依赖于进程存活
  • 利用Handler实现定时任务的类:HandlerTimer
  • 如果时间较长,则需要使用AlarmManager
  • 另外,我们对于这种业务应该优先考虑是否可以基于事件通知。
  • 如果是加入媒体库的文件,我们可以使用registerContentObserver监听媒体库文件变化。

[Android] 使用Handler到底需不需要使用弱引用,什么时候情况下用

使用Handler到底需不需要使用弱引用,什么时候情况下用

  • 正常境况下的引用都为强引用,其特点是及时内存溢出也不可以被回收
1
ArrayList list = new ArrayList();
  • 弱引用则会在垃圾回收时被回收掉,因而弱引用解决内存泄露的一种方法。
1
2
3
ArrayList list = new ArrayList();
WeakReference<ArrayList> listWeakRef = new WeakReference<ArrayList>(list);
ArrayList myList = listWeakRef.get();
  • Handler是否需要设置弱引用,取决于它是否可能发生内存泄露

Handler内存泄露的场景

  • Message的target变量实际是Handler对象
  • Message存放在MessageQueue中
  • MessageQueue通常为Looper持有
  • Looper和可以认为和线程生命周期相同
  • 通常情况下,我们使用匿名内部类的形式创建Handler,而匿名内部类(非静态内部类)会隐式持有外部类的引用。即如下的mHandler会隐式持有Activity的实例引用。
1
2
3
4
5
6
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
    }
};
  • 如果有一个延迟很久的消息,可能会导致Activity内存泄露
  • 可以使用弱引用解决内存泄露问题
  • 也可以在Activity onDestory方法中调用handler.removeCallbacksAndMessages(null);

相关文章