Dagger2是由Google接手开发,最早的版本Dagger1 是由Square公司开发的,大神JakeWharton最近也从 Square 公司跳槽到 Google。1
2A fast dependency injector for Android and Java
Android和Java的依赖快速注入器
快速自动的构建出我们所需要的依赖对象,这里的依赖对象可以理解为某一个成员变量。例如在 MVP
中,VP
层就是互相关联的, V
要依赖对应的 P
,而 P
也要依赖对应的 V
。Dagger2 能解决的就是这种依赖关系,通过注入的方式,将双方的耦合再次降低,在实际的使用中体现为一个注解想要的对象就创建好了,咱们不用再去管理所依赖对象的创建等情况了。
如果在 MainActivity
中,有 Tinno
的实例,则称 MainActivity
对 Tinno
有一个依赖。如果不用Dagger2的情况下我们应该这么写:1
2
3
4
5Tinno mTinno;
public MainActivity() {
mTinno = new Tinno();
}
上面例子面临着一个问题,一旦某一天Tinno
的创建方式(如构造参数)发生改变,那么你不但需要修改MainActivity
中创建Tinno
的代码,还要修改其他所有地方创建Tinno
的代码。如果我们使用了Dagger2的话,就不需要管这些了,只需要在需要Tinno
的地方写下:1
2
Tinno mTinno;
Android Studio 2.2以前的版本需要使用Gradle插件android-apt
(Annotation Processing Tool),协助Android Studio处理annotation processors
;annotationProcessor
就是APT工具中的一种,是google开发的内置框架,不需要引入,所以可以像下面这样直接使用。1
2
3
4
5// Add Dagger dependencies
dependencies {
compile 'com.google.dagger:dagger:2.4'
annotationProcessor 'com.google.dagger:dagger-compiler:2.4'
}
Dagger2 通过注解来生成代码,定义不同的角色,主要的注解如下:
Module
类里面的方法专门提供依赖,所以我们定义一个类,用@Module
注解,这样Dagger在构造类的实例的时候,就知道从哪里去找到需要的依赖。Module
中,我们定义的方法是用这个注解,以此来告诉Dagger2
我们想要构造对象并提供这些依赖。Dagger2
这个类或者字段需要依赖注入。这样,Dagger2
就会构造一个这个类的实例并满足他们的依赖。Component
从根本上来说就是一个注入器,也可以说是@Inject
和@Module
的桥梁,它的主要作用就是连接这两个部分。将Module
中产生的依赖对象自动注入到需要依赖实例的Container中。Component
和Module
提供对象的方法,Dagger2
可以通过自定义注解限定注解作用域,来管理每个对象实例的生命周期。Context
,所以我们就可以定义Qualifier
注解@ApplicationQualifier
和@ActivityQualifier
,这样当注入一个Context
的时候,我们就可以告诉Dagger2
我们想要哪种类型的Context
。 Dagger2要实现一个完整的依赖注入,通常必不可少的元素有三种:Module,Component,Container。为了便于理解,其实可以把component
想象成针管
,module
是注射器
,里面的依赖对象
是待注入的药水
,build方法
是插进患者(Container)
,inject方法
的调用是推动活塞
。
声明需要依赖的对象:使用了注解方式,还是以Tinno
为例,使得Dagger2能找到它。1
2
3
4
5public class Tinno {
//这里可以看到加入了注解方式
public Tinno() {
}
}
声明Component
接口:声明完后rebuild一下工程,使其自动生成Component
实现类DaggerMainActivityComponent
。1
2
3
4
5
6//用这个标注标识是一个连接器
public interface MainActivityComponent {
//这个连接器要注入的对象。这个inject标注的意思是,我后面的参数对象里面有标注为@Inject的属性,这个标注的属性是需要这个连接器注入进来的。
void inject(MainActivity activity);
}
在使用的地方注入,这里是 MainActivity:
1 | public class MainActivity extends AppCompatActivity { |
这是最简单的一种使用了。首先我们看到,第一印象是我去😲,这个更复杂了啊😂😂。我只能说确实,因为这个是它对的最基础的使用,看起来很笨拙,但是当它在大型项目里面,在依赖更多的情况下,则会发生质的飞跃,会发现它非常好用,并且将你需要传递的参数都隐藏掉,来实现解耦。
细心的朋友发现了,我在结构中说 Dagger2结构的时候提到通常必不可少的三元素,这个例子只用到了 Component和Container,而 Module 并未提及,通过以下这个例子,能更加深刻的理解 Module 的作用。
实现一个 MainModule,提供一些实例构造,通过 Component 联系起来。1
2
3
4
5
6
7
8//实现一个类,标注为 Module
public class MainModule {
//实现一些提供方法,供外部使用
public Tinno provideTinno(){
return new Tinno();
}
}
在 MainComponent 中,指明 Component 查找 Module 的位置1
2
3
4 (modules = MainModule.class)
public interface MainActivityComponent {// 通常定义为接口,Dagger2框架将自动生成Component的实现类,对应的类名是Dagger×××××,这里对应的实现类是DaggerMainActivityComponent
void inject(MainActivity activity);// 注入到MainActivity(Container)的方法,方法名一般使用inject
}
最后我们的Tinno
类中的@Inject
和构造函数可以去掉了(亲测不去掉也是可以正常运行的,此时也是使用Module
中提供的对象,具体可以通过后面分享的@Scope
来验证,这样说明:Component会首先从Module维度中查找类实例,若找到就用Module维度创建类实例,并停止查找Inject维度,否则才是从Inject维度查找类实例,所以创建类实例级别Module维度要高于Inject维度。),如下所示。1
2public class Tinno {
}
注入使用的地方完全不用修改,也能得到和之前例子一样的结果。
上面的例子@Provides
标注的方法是没有输入参数的,Module
中@Provides
标注的方法是可以带输入参数的,其参数值可以由Module
中的其他被@Provides
标注的方法提供。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//实现一个类,标注为 Module
public class MainModule {
private Context mContext;
public MainModule(Context context) {
mContext = context;
}
//实现一些提供方法,供外部使用
public Tinno provideTinno(Gson gson, CameraTeam cameraTeam) {
return new Tinno(mContext, gson, cameraTeam);
}
public Gson provideGson() {
return new GsonBuilder()
.excludeFieldsWithModifiers(Modifier.PROTECTED)//忽略protected字段
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.create();
}
// @Provides
// public CameraTeam provideCameraTeam() {
// return new CameraTeam();
// }
}
如果找不到被@Provides
注释的方法提供对应参数对象的话,将会自动调用被@Inject
注释的构造方法生成相应对象。1
2
3
4
5
6public class CameraTeam {
public CameraTeam() {
}
}
由于我们修改了MainModule
,所以对应注入的地方要稍微修改一下:1
2//注意此处比之前多了.mainModule(new MainModule(getApplicationContext()))
DaggerMainActivityComponent.builder().mainModule(new MainModule(getApplicationContext())).build().inject(this);
思考
通过上面3个例子@Provides
和@Inject
两种方式提供对象的区别?
一个Component
可以添加多个Module
,这样Component
获取依赖时候会自动从多个Module
中查找获取。添加多个Module
有两种方法,一种是在Component
的注解@Component(modules={××××,×××})
中添加多个modules
。1
2
3
4//直接在Component引用多个 Module (modules={MainModule.class,ModuleA.class,ModuleB.class,ModuleC.class})
public interface MainActivityComponent {
...
}
另外一种添加多个Module
的方法可以使用@Module
的 includes
的方法(includes={××××,×××})
。1
2
3
4
5
6
7
8//先在一个 Module 中includes其他 Module (includes={ModuleA.class,ModuleB.class,ModuleC.class})
public class MainModule {
...
}
//只有一个 Module 时可以不用{} (modules={MainModule.class})
public interface MainActivityComponent {
...
}
如果我们在 Module
中有重复的类型返回,例如我定义两个 Context
类型的Provides
在 Module
中的话,编译直接会报错:
1 | Error:(16, 10) 错误: android.content.Context is bound multiple times: |
那如果我们真的需要注入同一类型多次呢,这个问题总会有解决方案的吧?要是真的这么坑估计也没人用 Dagger2 了吧!哈哈。。。😂 其实 Dagger2 为我们提供了两种方式来解决这个问题:
@Qualifier
的注解来区分@Named("xx")
的注解。@Named 方式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//实现一个类,标注为 Module
public class MainModule {
private Context mApplicationContext;
private Context mActivityContext;
public MainModule(Context context, Context activityContext) {
mApplicationContext = context;
mActivityContext = activityContext;
}
//实现一些提供方法,供外部使用
public Tinno provideTinno(@Named("application")/*使用的是 application*/Context context, Gson gson, CameraTeam cameraTeam) {
return new Tinno(context, gson, cameraTeam);
}
"application") //标注为 application (
public Context provideApplicationContext() {
return mApplicationContext;
}
"activity") //标注为 activity (
public Context provideActivityContext() {
return mActivityContext;
}
...
}
@Qualifier方式
1 |
|
1 |
|
1 |
|
使用哪种方式就仁者见仁智者见智了,但个人推荐使用@Qualifier
,毕竟输入太多字符串容易出错。
有时我们需要依赖一个组件,这个最常见的用法是,如果我们定义了 MainActivity
的 MainComponent
,并且它依赖咱们的 AppComponent
里面的 IRepositoryManager
的话就要这样定义了:1
2
3
4
5
(dependencies = AppComponent.class, modules = MainPresenterModule.class)
public interface MainComponent {
void inject(MainActivity activity);
}
在 AppComponent
中需要将获取 IRepositoryManager
的方法暴露出来,不然还是无法注入成功的。
1 |
|
那如果我觉得暴露这些方法太麻烦了,那需要怎么办呢?最简单就是使用 @SubComponent
,在所属的父 Component
中定义一个 SubComponent
,该 SubComponent
中将会包含父 Component
的所有方法,父 Component
不显示声明都可以。
1 |
|
1 |
|
在注入的时候直接使用父组件的mainComponent(MainPresenterModule module)
包含子组件的module
:
1 |
|
组件依赖和子组件的区别:
组件依赖 | 子组件 |
---|---|
1. 保持两个 Component 都独立,没有任何关联 2. 明确的告诉别人这个 Component 所依赖的 Component 3. 两个拥有依赖关系的 Component 是不能有相同 @Scope 注解的 4. 依赖的组件会生成Dagger…Component | 1. 保持两个 Component 内聚 2. 不关心这个 Component 依赖哪个 Component 3. 可以使用相同的@Scope注解 4. 子组件的组件不会生成Dagger…Component |
在上面的比喻中,一针扎进去,是啥都给你打进去了,那么如果有些我想要在调用的时候才加载呢?这里 Dagger2 提供了 Lazy<T>
的方式来注入;同时相反的提供一个强制加载方式Provider<T>
,每次调用get都会调用Module的Provides方法一次,对应的获取就是:1
2
3
4
5
6
7
8public class Container{
//延迟加载 Lazy<Tinno> mTinnoLazy;
//实现强制加载,每次调用get都会调用Module的Provides方法一次,和懒加载模式正好相反,比如我们需要一次性创建出10个Tinno 对象 Provider<Tinno> mTinnoProvider;
public void init(){
DaggerComponent.create().inject(this);
Tinno tinno = mTinnoLazy.get(); //调用get时才创建b
}
}
@Scope
详解@Scope 是什么
Scope
翻译过来就是辖域,再结合到计算机上,其实就是作用域的意思,学过高级语言的应该都知道设计模式中一个模式叫做单例模式,单例即为全局中该对象的实例只存在一个,而在 Dagger2 中,@scope
的一个默认实现就是 @Singleton
,也是Dagger2唯一自带的Scope注解,下面是@Singleton
的源码,乍一看,很神奇啊,仅仅使用一个注解就可以实现单例!1
2
3
4
(RUNTIME)
public Singleton{}
可以看到定义一个Scope
注解,通常需要添加以下三部分:
@Scpoe 怎么用
那么接下来我们就看一下它的使用。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//普通的对象
public class Tinno {}
//声明Module
public class MainModule {
Tinno provideTinno() {
return new Tinno();
}
}
(modules = UserModule.class)
public interface MainActivityComponent {//同一个Component可以声明多个注入Container
void inject(MainActivity activity);
void inject(SecondActivity activity);
}
我们创建一个普通的 Tinno
类,然后创建它的Module
,并且用 @Singleton
标记该 Tinno
返回对象,最后我们再创建它的 Component
,然后用 @Singleton
标记这个 Component
。这是一个标准的套路流程。接下来我们创建一个 MainActivity
和一个 SecondActivity
,代码如下: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
46public class MainActivity extends AppCompatActivity {
Tinno mTinno1;
Tinno mTinno2;
private TextView mContentTextView;
private Button mContentButton;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mContentTextView = (TextView) findViewById(R.id.tv_content);
mContentButton = (Button) findViewById(R.id.btn_content);
// [1]
MainActivityComponent component = DaggerMainActivityComponent.create();
component.inject(this);
// 第一行为 mTinno1 的信息,第二行为 mTinno2 的信息,第三行为该类中 MainActivityComponent 的信息
mContentTextView.setText(mTinno1.toString() + "\n" + mTinno2.toString()+"\n"+ component.toString());
mContentButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, SecondActivity.class));
}
});
}
}
public class SecondActivity extends AppCompatActivity {
Tinno mTinno;
private TextView mContentTextView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
mContentTextView = (TextView) findViewById(R.id.tv_content);
// [2]
MainActivityComponent component = DaggerMainActivityComponent.create();
component.inject(this);
// 第一行为 mTinno 的信息,第二行为该类中 MainActivityComponent 的信息
mContentTextView.setText(mTinno.toString() + "\n" + component.toString());
}
}
运行结果如下图所示,没有问题的,单例实现成功了,发现两个 Tinno
的地址是一样的。
我们仅仅通过一个 @Singleton
标记就使得对象实现了单例模式,接下来我们点一下按钮跳转到 SecondActivity
中,如下图所示:
但是此时我们发现,不对啊,SecondActivity
的 Tinno
对象的地址和 MainActivity
中的 Tinno
对象地址并不一样啊,这个单例好像失效了啊!事实上并不是这样,那么为什么这个单例“失效”了呢?细心的小伙伴们已经看到了,两个 Activity
中的 Component
对象的地址是并不一样的,这样就好理解了 ——— 由于 Component
对象不是同一个,当然它们注入的对象也不会是同一个。那么我们如何解决这个问题呢?
我们在 Application
层初始化 MainActivityComponent
,然后在 Activity
中直接获取这个 MainActivityComponent
对象,由于 Application
在全局中只会初始化一次, 所以 Application
中的 MainActivityComponent
对象只初始化一次,我们每次在 Activity
中获取 Application
中的这个 MainActivityComponent
当然就是同一个的啦。Application
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13public class App extends Application {
MainActivityComponent mComponent;
public void onCreate() {
super.onCreate();
mComponent = DaggerMainActivityComponent.create();
}
public MainActivityComponent getComponent() {
return mComponent;
}
}
我们只需要将 [1] 和 [2] 处的代码更改成 MainActivityComponent component = ((App)getApplication()).getComponent();
这样我们不就能将我们的 MainActivityComponent
“单例”化了吗?截图这里就不再贴出了。
自定义@Scpoe
Dagger2中@Singleton
和自己定义的@ActivityScope
、@ApplicationScope
等代码上并没有什么区别,区别是在那种Component
依赖的Component
的情况下,两个Component
的@Scope
不能相同,既然没什么区别,那为什么还要这么做呢?是因为这样标示可以清晰的区分Component
依赖的层次,方便理清我们的代码逻辑层次,如下为自定义的ActivityScope
:1
2
3
4
5
6
(RUNTIME)
public ActivityScope {
}
有@Scope
注解和没@Scope
注解的编译时生成代码的区别,在编译生成的DaggerMainActivityComponent
的initialize
函数代码中我们可以看到如下:
1 | private void initialize(final Builder builder) { |
有@Scope
类注解的@Provider
生成的代码,外层多了一层DoubleCheck.provider(…);
没有@Scope
类注解的则是直接create一个新的实例。关于DoubleCheck
,简单来说就是加了@Scope
的Provider
,Dagger
会缓存一个实例在DaggerMainComponent
中,在DaggerMainComponent
中保持单例,缓存的provide
跟随DaggerMainComponent
的生命周期,DaggerMainComponent
被销毁时,provider
也被销毁,这就是局部单例的概念,假如你的DaggerMainComponent
是在你应用的application
中,则就形成了全局单例。
增加开发效率、省去重复的简单体力劳动
首先new一个实例的过程是一个重复的简单体力劳动,Dagger2完全可以把new一个实例的工作做了,因此我们把主要精力集中在关键业务上、同时也能增加开发效率上。省去写单例的方法,并且也不需要担心自己写的单例方法是否线程安全,自己写的单例是懒汉模式还是饿汉模式。因为Dagger2都可以把这些工作做了。
更好的管理类实例
每个app中的ApplicationComponent管理整个app的全局类实例,所有的全局类实例都统一交给ApplicationComponent管理,并且它们的生命周期与app的生命周期一样。每个页面对应自己的Component,页面Component管理着自己页面所依赖的所有类实例。因为Component,Module,整个app的类实例结构变的很清晰。
解耦
假如不用Dagger2的话,一个类的new代码是非常可能充斥在app的多个类中的,假如该类的构造函数发生变化,那这些涉及到的类都得进行修改。设计模式中提倡把容易变化的部分封装起来。
本文所演示的代码在此下载:Dagger2Sample
MVP使用 Dagger2的例子在此下载:MaterialWeather
]]>1 | 国家 | 后缀 |
申请CAMERA
和RECORD_AUDIO
权限。
CameraDevice.StateCallback
回调获取打开结果。 其中openCamera函数声明如下(cameraId
:Camera的id,通常0表示后置摄像头,1表示前置摄像头;callback
: CameraDevice.StateCallback
相机状态回调; handler
:执行回调操作的hanlder,为空时,在主线程中执行,如果传入异步线程的handler,则在异步线程中执行):
1 | (android.Manifest.permission.CAMERA) |
完整打开Camera代码如下:
1 | /** |
openCamera
方法后会回调CameraDevice.StateCallback
这个接口,在该接口里重写onOpened
函数,开启预览。1 | /** |
CameraDevice
来创建会话createCaptureSession
。 声明如下(outputs
:预览输出载体,比如TextureView的getSurfaceTexture(),拍照时ImageReader的getSurface(),录像时MediaRecorder的getSurface();callback
:摄像头采集状态回调;handler
:同openCamera):
1 | public abstract void createCaptureSession(@NonNull List<Surface> outputs, |
完整的startPreview函数如下: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/**
* Start the camera preview.
*/
private void startPreview() {
if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {
return;
}
try {
closePreviewSession();
SurfaceTexture texture = mTextureView.getSurfaceTexture();//获取TextureView的SurfaceTexture,作为预览输出载体
assert texture != null;
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
//创建CameraRequest.Builder, 当程序调用setRepeatingRequest()方法进行预览时,
// 或调用capture()方法进行拍照时,都需要传入CameraRequest参数.
// CameraRequest代表了一次捕获请求,用于描述捕获图片的各种参数设置,
// 比如对焦模式、曝光模式……程序需要对照片所做的各种控制,都通过CameraRequest参数进行设置。
// 可以理解一个请求参数一样,CameraRequest.Builder则负责生成CameraRequest对象。
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
Surface previewSurface = new Surface(texture);
mPreviewBuilder.addTarget(previewSurface);
mCameraDevice.createCaptureSession(Arrays.asList(previewSurface), new CameraCaptureSession.StateCallback() {
public void onConfigured(CameraCaptureSession cameraCaptureSession) {
mPreviewSession = cameraCaptureSession;
mPreviewBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
//不停的发送获取图像请求,完成连续预览
mPreviewSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler);
}
public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
Activity activity = getActivity();
if (null != activity) {
Toast.makeText(activity, "Failed", Toast.LENGTH_SHORT).show();
}
}
}, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
setRepeatingRequest和capture方法其实都是向相机设备发送获取图像的请求,但是capture就获取那么一次,而setRepeatingRequest就是不停的获取图像数据,所以呢,使用capture就像拍照一样,图像就停在那里,但是setRepeatingRequest一直在发送和获取,所以需要连拍的时候就调用它,然后在onCaptureCompleted中保存图像就行了。(注意了,图像的预览也是用的setRepeatingRequest,只是不处理数据)
当用户点击录像按钮时,先配置好MediaRecorder
相关参数,然后重启预览,并在预览开始时调用MediaRecorder.start()
,正式开始录像:
1 | private void startRecordingVideo() { |
1 | private void stopRecordingVideo() { |
无需修改原app的代码,无需重新编译,就能覆盖app内的部分资源,从而实现修改app的部分UI,后面简称RRO。
应用通过 getString/getDrawable去调用某个资源时,会将这个resources ID 作为参数传给 framework 层。同一名称但不同状态的 resources ID 是一样的,比如不同分辨率但名称相同的图片分别被放置在了drawable-hdpi/drawable-ldpi/drawable-mdpi下,但在编译时针对该图片生成的resources ID只有一个。
为了快速查找到指定的资源,Apk编译的时候会把Java文件里面的R.String.app_name替换成ox7f123456这种格式的值
每个apk里面都有一个文件(resources.arsc)记录着指定的resource_id对应的资源类型,如果是string类型,则记录的这个资源名称对应的所有语言的翻译,如果是drawable类型,则记录着哪些分辨率底下有这个资源。
1 | <?xml version="1.0" encoding="utf-8"?> |
1 | ifeq ($(strip $(MYOS_APE_GALLERY31_SUPPORT)), yes) |
alogcat
可以过滤打印出对应包名的log:1 | # 作用:能够通过进程名显示log |
tingpng
批量压缩图片资源的脚本:1 |
|
编辑.bashrc文件,将下面的代码加入到文件的最后处,保存退出,执行加载命令source ./.bashrc
:
1 | ## Parses out the branch name from .git/HEAD: |
git diffall branch_a...branch_b
来对比两个分支间差异配置:在~/bin目录下新建git-diffall文件:
1 |
|
然后配置~/.gitconfig
文件:
1 | [color] |
1 | ## Parses out the branch name from .git/HEAD: |
对于任何一个文件,在 Git 内都只有三种状态:已修改(modified)、已暂存(staged)、已提交(committed):
已提交表示该文件已经被安全地保存在本地数据库中了.
由此可见,rebase过程可能产生冲突,而且可能需要解决多次
注意点:
注意点:
注意点:
操作步骤及注意事项:
操作步骤及注意事项
操作步骤及注意事项
Android 开发中,从原生的 HttpUrlConnection 到经典的 Apache 的 HttpClient,再到对前面这些网络基础框架的封装,比如 Volley、Async Http Client,Http 相关开源框架的选择还是很多的,其中由著名的 Square 公司开源的 Retrofit 更是以其简易的接口配置、强大的扩展支持、优雅的代码结构受到大家的追捧。也正是由于 Square 家的框架一如既往的简洁优雅,所以我一直在想,Square 公司是不是只招处女座的程序员?
单从 Retrofit 这个单词,你似乎看不出它究竟是干嘛的,当然,我也看不出来 :)逃。。1
2Retrofitting refers to the addition of new technology or features to older systems.
—From Wikipedia
于是我们就明白了,冠以 Retrofit 这个名字的这个家伙,应该是某某某的 『Plus』 版本了。
Retrofit 是一个 RESTful 的 HTTP 网络请求框架的封装。注意这里并没有说它是网络请求框架,主要原因在于网络请求的工作并不是 Retrofit 来完成的。Retrofit 2.0 开始内置 OkHttp,前者专注于接口的封装,后者专注于网络请求的高效,二者分工协作,宛如古人的『你耕地来我织布』,小日子别提多幸福了。
我们的应用程序通过 Retrofit 请求网络,实际上是使用 Retrofit 接口层封装请求参数、Header、Url 等信息,之后由 OkHttp 完成后续的请求操作,在服务端返回数据之后,OkHttp 将原始的结果交给 Retrofit,后者根据用户的需求对结果进行解析的过程。
讲到这里,你就会发现所谓 Retrofit,其实就是 Retrofitting OkHttp 了。
多说无益,不要来段代码陶醉一下。使用 Retrofit 非常简单,首先你需要在你的 build.gradle 中添加依赖:1
compile 'com.squareup.retrofit2:retrofit:2.0.2'
你一定是想要访问 GitHub 的 api 对吧,那么我们就定义一个接口:1
2
3
4public interface GitHubService {
"users/{user}/repos") (
Call<List<Repo>> listRepos("user") String user); (
}
接口当中的 listRepos 方法,就是我们想要访问的 api 了:https://api.github.com/users/{user}/repos
其中,在发起请求时, {user} 会被替换为方法的第一个参数 user。
好,现在接口有了,我们要构造 Retrofit 了:1
2
3
4
5Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
GitHubService service = retrofit.create(GitHubService.class);
这里的 service 就好比我们的快递哥,还是往返的那种哈~1
Call<List<Repo>> repos = service.listRepos("octocat");
发请求的代码就像前面这一句,返回的 repos 其实并不是真正的数据结果,它更像一条指令,你可以在合适的时机去执行它:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 同步调用
List<Repo> data = repos.execute();
// 异步调用
repos.enqueue(new Callback<List<Repo>>() {
public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {
List<Repo> data = response.body();
}
public void onFailure(Call<List<Repo>> call, Throwable t) {
t.printStackTrace();
}
});
啥感觉?有没有突然觉得请求接口就好像访问自家的方法一样简单?呐,前面我们看到的,就是 Retrofit 官方的 demo 了。你以为这就够了?噗~怎么可能。。
Retrofit 支持的协议包括 GET/POST/PUT/DELETE/HEAD/PATCH,当然你也可以直接用 HTTP 来自定义请求。这些协议均以注解的形式进行配置,比如我们已经见过 GET 的用法:1
2"users/{user}/repos") (
Call<List<Repo>> listRepos("user") String user); (
这些注解都有一个参数 value,用来配置其路径,比如示例中的 users/{user}/repos,我们还注意到在构造 Retrofit 之时我们还传入了一个 baseUrl(“https://api.github.com/"),请求的完整 Url 就是通过 baseUrl 与注解的 value(下面称 “path“ ) 整合起来的,具体整合的规则如下:
建议采用第二种方式来配置,并尽量使用同一种路径形式。如果你在代码里面混合采用了多种配置形式,恰好赶上你哪天头晕眼花,信不信分分钟写一堆 bug 啊哈哈。
发请求时,需要传入参数,Retrofit 通过注解的形式令 Http 请求的参数变得更加直接,而且类型安全。
1 | "/list") ( |
Query 其实就是 Url 中 ‘?’ 后面的 key-value,比如:http://www.println.net/?cate=android
这里的 cate=android 就是一个 Query,而我们在配置它的时候只需要在接口方法中增加一个参数,即可:1
2
3
4interface PrintlnServer{
"/") (
Call<String> cate(@Query("cate") String cate);
}
这时候你肯定想,如果我有很多个 Query,这么一个个写岂不是很累?而且根据不同的情况,有些字段可能不传,这与方法的参数要求显然也不相符。于是,打群架版本的 QueryMap 横空出世了,使用方法很简单,我就不多说了。
其实我们用 POST 的场景相对较多,绝大多数的服务端接口都需要做加密、鉴权和校验,GET 显然不能很好的满足这个需求。使用 POST 提交表单的场景就更是刚需了,怎么提呢?1
2
3
4
5
"/") (
Call<ResponseBody> example(
@Field("name") String name,
@Field("occupation") String occupation);
其实也很简单,我们只需要定义上面的接口就可以了,我们用 Field 声明了表单的项,这样提交表单就跟普通的函数调用一样简单直接了。
等等,你说你的表单项不确定个数?还是说有很多项你懒得写?Field 同样有个打群架的版本——FieldMap,赶紧试试吧~~
这个是用来上传文件的。话说当年用 HttpClient 上传个文件老费劲了,一会儿编码不对,一会儿参数错误(也怪那时段位太低吧TT)。。。可是现在不同了,自从有了 Retrofit,妈妈再也不用担心文件上传费劲了~~~1
2
3
4
5
6public interface FileUploadService {
"upload") (
Call<ResponseBody> upload(@Part("description") RequestBody description,
@Part MultipartBody.Part file);
}
如果你需要上传文件,和我们前面的做法类似,定义一个接口方法,需要注意的是,这个方法不再有 @FormUrlEncoded 这个注解,而换成了 @Multipart,后面只需要在参数中增加 Part 就可以了。也许你会问,这里的 Part 和 Field 究竟有什么区别,其实从功能上讲,无非就是客户端向服务端发起请求携带参数的方式不同,并且前者可以携带的参数类型更加丰富,包括数据流。也正是因为这一点,我们可以通过这种方式来上传文件,下面我们就给出这个接口的使用方法: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//先创建 service
FileUploadService service = retrofit.create(FileUploadService.class);
//构建要上传的文件
File file = new File(filename);
RequestBody requestFile =
RequestBody.create(MediaType.parse("application/otcet-stream"), file);
MultipartBody.Part body =
MultipartBody.Part.createFormData("aFile", file.getName(), requestFile);
String descriptionString = "This is a description";
RequestBody description =
RequestBody.create(
MediaType.parse("multipart/form-data"), descriptionString);
Call<ResponseBody> call = service.upload(description, body);
call.enqueue(new Callback<ResponseBody>() {
public void onResponse(Call<ResponseBody> call,
Response<ResponseBody> response) {
System.out.println("success");
}
public void onFailure(Call<ResponseBody> call, Throwable t) {
t.printStackTrace();
}
});
在实验时,我上传了一个只包含一行文字的文件:
Visit me: http://www.println.net
那么我们去服务端看下我们的请求是什么样的:
HEADERS1
2
3
4
5
6
7
8
9
10Accept-Encoding: gzip
Content-Length: 470
Content-Type: multipart/form-data; boundary=9b670d44-63dc-4a8a-833d-66e45e0156ca
User-Agent: okhttp/3.2.0
X-Request-Id: 9d70e8cc-958b-4f42-b979-4c1fcd474352
Via: 1.1 vegur
Host: requestb.in
Total-Route-Time: 0
Connection: close
Connect-Time: 0
FORM/POST PARAMETERS1
description: This is a description
RAW BODY1
2
3
4
5
6
7
8
9
10
11
12
13
14--9b670d44-63dc-4a8a-833d-66e45e0156ca
Content-Disposition: form-data; name="description"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 21
This is a description
--9b670d44-63dc-4a8a-833d-66e45e0156ca
Content-Disposition: form-data; name="aFile"; filename="uploadedfile.txt"
Content-Type: application/otcet-stream
Content-Length: 32
Visit me: http://www.println.net
--9b670d44-63dc-4a8a-833d-66e45e0156ca--
我们看到,我们上传的文件的内容出现在请求当中了。如果你需要上传多个文件,就声明多个 Part 参数,或者试试 PartMap。
前面我为大家展示了如何用 Retrofit 上传文件,这个上传的过程其实。。还是有那么点儿不够简练,我们只是要提供一个文件用于上传,可我们前后构造了三个对象:
天哪,肯定是哪里出了问题。实际上,Retrofit 允许我们自己定义入参和返回的类型,不过,如果这些类型比较特别,我们还需要准备相应的 Converter,也正是因为 Converter 的存在, Retrofit 在入参和返回类型上表现得非常灵活。
下面我们把刚才的 Service 代码稍作修改:1
2
3
4
5
6
7public interface FileUploadService {
"upload") (
Call<ResponseBody> upload(@Part("description") RequestBody description,
//注意这里的参数 "aFile" 之前是在创建 MultipartBody.Part 的时候传入的
@Part("aFile") File file);
}
现在我们把入参类型改成了我们熟悉的 File,如果你就这么拿去发请求,服务端收到的结果会让你哭了的。。。
RAW BODY1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16--7d24e78e-4354-4ed4-9db4-57d799b6efb7
Content-Disposition: form-data; name="description"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 21
This is a description
--7d24e78e-4354-4ed4-9db4-57d799b6efb7
Content-Disposition: form-data; name="aFile"
Content-Transfer-Encoding: binary
Content-Type: application/json; charset=UTF-8
Content-Length: 35
// 注意这里!!之前是文件的内容,现在变成了文件的路径
{"path":"samples/uploadedfile.txt"}
--7d24e78e-4354-4ed4-9db4-57d799b6efb7--
服务端收到了一个文件的路径,它肯定会觉得
好了,不闹了,这明显是 Retrofit 在发现自己收到的实际入参是个 File 时,不知道该怎么办,情急之下给 toString了,而且还是个 JsonString(后来查证原来是使用了 GsonRequestBodyConverter。。)。
接下来我们就自己实现一个 FileRequestBodyConverter,1
2
3
4
5
6
7
8
9
10
11
12
13
14static class FileRequestBodyConverterFactory extends Converter.Factory {
public Converter<File, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
return new FileRequestBodyConverter();
}
}
static class FileRequestBodyConverter implements Converter<File, RequestBody> {
public RequestBody convert(File file) throws IOException {
return RequestBody.create(MediaType.parse("application/otcet-stream"), file);
}
}
在创建 Retrofit 的时候记得配置上它:1
addConverterFactory(new FileRequestBodyConverterFactory())
这样,我们的文件内容就能上传了。来,看下结果吧:
RAW BODY1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17--25258f46-48b0-4a6b-a617-15318c168ed4
Content-Disposition: form-data; name="description"
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 21
This is a description
--25258f46-48b0-4a6b-a617-15318c168ed4
//注意看这里,filename 没了
Content-Disposition: form-data; name="aFile"
//多了这一句
Content-Transfer-Encoding: binary
Content-Type: application/otcet-stream
Content-Length: 32
Visit me: http://www.println.net
--25258f46-48b0-4a6b-a617-15318c168ed4--
文件内容成功上传了,当然其中还存在一些问题,这个目前直接使用 Retrofit 的 Converter 还做不到,原因主要在于我们没有办法通过 Converter 直接将 File 转换为 MultiPartBody.Part,如果想要做到这一点,我们可以对 Retrofit 的源码稍作修改,这个我们后面再谈。
前面我们为大家简单示例了如何自定义 RequestBodyConverter,对应的,Retrofit 也支持自定义 ResponseBodyConverter。
我们再来看下我们定义的接口:1
2
3
4public interface GitHubService {
"users/{user}/repos") (
Call<List<Repo>> listRepos("user") String user); (
}
返回值的类型为 List
问题来了,如果请求得到的 Json 字符串与返回值类型不对应,比如:
接口返回的 Json 字符串:1
{"err":0, "content":"This is a content.", "message":"OK"}
返回值类型:1
2
3
4
5class Result{
int code;//等价于 err
String body;//等价于 content
String msg;//等价于 message
}
哇,这时候肯定有人想说,你是不是脑残,偏偏跟服务端对着干?哈哈,我只是示例嘛,而且在生产环境中,你敢保证这种情况不会发生??
这种情况下, Gson 就是再牛逼,也只能默默无语俩眼泪了,它哪儿知道字段的映射关系怎么这么任性啊。好,现在让我们自定义一个 Converter 来解决这个问题吧!1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25static class ArbitraryResponseBodyConverterFactory extends Converter.Factory{
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
return super.responseBodyConverter(type, annotations, retrofit);
}
}
static class ArbitraryResponseBodyConverter implements Converter<ResponseBody, Result>{
public Result convert(ResponseBody value) throws IOException {
RawResult rawResult = new Gson().fromJson(value.string(), RawResult.class);
Result result = new Result();
result.body = rawResult.content;
result.code = rawResult.err;
result.msg = rawResult.message;
return result;
}
}
static class RawResult{
int err;
String content;
String message;
}
当然,别忘了在构造 Retrofit 的时候添加这个 Converter,这样我们就能够愉快的让接口返回 Result 对象了。
注意!!Retrofit 在选择合适的 Converter 时,主要依赖于需要转换的对象类型,在添加 Converter 时,注意 Converter 支持的类型的包含关系以及其顺序。
前一个小节我们把 Retrofit 的基本用法和概念介绍了一下,如果你的目标是学会如何使用它,那么下面的内容你可以不用看了。
不过呢,我就知道你不是那种浅尝辄止的人!这一节我们主要把注意力放在 Retrofit 背后的魔法上面~~
前面讲了这么久,我们始终只看到了我们自己定义的接口,比如:1
2
3
4public interface GitHubService {
"users/{user}/repos") (
Call<List<Repo>> listRepos("user") String user); (
}
而真正我使用的时候肯定不能是接口啊,这个神秘的家伙究竟是谁?其实它是 Retrofit 创建的一个代理对象了,这里涉及点儿 Java 的动态代理的知识,直接来看代码: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
30public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
//这里返回一个 service 的代理对象
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
public Object invoke(Object proxy, Method method, Object... args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
//DefaultMethod 是 Java 8 的概念,是定义在 interface 当中的有实现的方法
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
//每一个接口最终实例化成一个 ServiceMethod,并且会缓存
ServiceMethod serviceMethod = loadServiceMethod(method);
//由此可见 Retrofit 与 OkHttp 完全耦合,不可分割
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
//下面这一句当中会发起请求,并解析服务端返回的结果
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
简单的说,在我们调用 GitHubService.listRepos 时,实际上调用的是这里的 InvocationHandler.invoke 方法~~
前面我们已经看到 Retrofit 为我们构造了一个 OkHttpCall ,实际上每一个 OkHttpCall 都对应于一个请求,它主要完成最基础的网络请求,而我们在接口的返回中看到的 Call 默认情况下就是 OkHttpCall 了,如果我们添加了自定义的 callAdapter,那么它就会将 OkHttp 适配成我们需要的返回值,并返回给我们。
先来看下 Call 的接口:1
2
3
4
5
6
7
8
9
10
11
12public interface Call<T> extends Cloneable {
//同步发起请求
Response<T> execute() throws IOException;
//异步发起请求,结果通过回调返回
void enqueue(Callback<T> callback);
boolean isExecuted();
void cancel();
boolean isCanceled();
Call<T> clone();
//返回原始请求
Request request();
}
我们在使用接口时,大家肯定还记得这一句:1
2Call<List<Repo>> repos = service.listRepos("octocat");
List<Repo> data = repos.execute();
这个 repos 其实就是一个 OkHttpCall 实例,execute 就是要发起网络请求。
OkHttpCall.execute1
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
34public Response<T> execute() throws IOException {
//这个 call 是真正的 OkHttp 的 call,本质上 OkHttpCall 只是对它做了一层封装
okhttp3.Call call;
synchronized (this) {
//处理重复执行的逻辑
if (executed) throw new IllegalStateException("Already executed.");
executed = true;
if (creationFailure != null) {
if (creationFailure instanceof IOException) {
throw (IOException) creationFailure;
} else {
throw (RuntimeException) creationFailure;
}
}
call = rawCall;
if (call == null) {
try {
call = rawCall = createRawCall();
} catch (IOException | RuntimeException e) {
creationFailure = e;
throw e;
}
}
}
if (canceled) {
call.cancel();
}
//发起请求,并解析结果
return parseResponse(call.execute());
}
我们看到 OkHttpCall 其实也是封装了 okhttp3.Call,在这个方法中,我们通过 okhttp3.Call 发起了进攻,额,发起了请求。有关 OkHttp 的内容,我在这里就不再展开了。
parseResponse 主要完成了由 okhttp3.Response 向 retrofit.Response 的转换,同时也处理了对原始返回的解析:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
ResponseBody rawBody = rawResponse.body();
//略掉一些代码
try {
//在这里完成了原始 Response 的解析,T 就是我们想要的结果,比如 GitHubService.listRepos 的 List<Repo>
T body = serviceMethod.toResponse(catchingBody);
return Response.success(body, rawResponse);
} catch (RuntimeException e) {
// If the underlying source threw an exception, propagate that rather than indicating it was
// a runtime exception.
catchingBody.throwIfCaught();
throw e;
}
}
至此,我们就拿到了我们想要的数据~~
前面我们已经提到过 CallAdapter 的事儿,默认情况下,它并不会对 OkHttpCall 实例做任何处理:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16final class DefaultCallAdapterFactory extends CallAdapter.Factory {
static final CallAdapter.Factory INSTANCE = new DefaultCallAdapterFactory();
public CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
... 毫不留情的省略一些代码 ...
return new CallAdapter<Call<?>>() {
... 省略一些代码 ...
public <R> Call<R> adapt(Call<R> call) {
//看这里,直接把传入的 call 返回了
return call;
}
};
}
}
现在的需求是,我想要接入 RxJava,让接口的返回结果改为 Observable:1
2
3
4
5
6public interface GitHub {
"/repos/{owner}/{repo}/contributors") (
Observable<List<Contributor>> contributors(
"owner") String owner, (
"repo") String repo); (
}
可不可以呢?当然是可以的,只需要提供一个 Adapter,将 OkHttpCall 转换为 Observable 即可呀!Retrofit 的开发者们早就想到了这个问题,并且为我们提供了相应的 Adapter:
RxJavaCallAdapterFactory
我们只需要在构造 Retrofit 时,添加它:1
addCallAdapterFactory(RxJavaCallAdapterFactory.create())
这样我们的接口就可以以 RxJava 的方式工作了。
好,歇会儿,抽一袋烟。。。
接着我们搞清楚 RxJavaCallAdapterFactory 是怎么工作的,首先让我们来看下 CallAdapter 的接口: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
31public interface CallAdapter<T> {
/*
*返回 Http 返回解析后的类型。需要注意的是这个并不是接口的返回类型,
*而是接口返回类型中的泛型参数的实参。
*/
Type responseType();
/*
* T 是我们需要转换成的接口返回类型,参数 call 其实最初就是 OkHttpCall 的实例
* 在这里 T 其实是 RxJava 支持的类型,比如 Observable
*/
<R> T adapt(Call<R> call);
//我们需要将 Factory 的子类对应的实例在构造 Retrofit 时添加到其中。
abstract class Factory {
//根据接口的返回类型(Observable<T>),注解类型等等来判断是否是当前 Adapter 支持的类型,不是则返回null
public abstract CallAdapter<?> get(Type returnType, Annotation[] annotations,
Retrofit retrofit);
//获取指定 index 的泛型参数的上限,比如对于 Map<String, ? extends Number>,index为 1 的参数上限是 Number
protected static Type getParameterUpperBound(int index, ParameterizedType type) {
return Utils.getParameterUpperBound(index, type);
}
/*
* 获取原始类型,比如 List<String> 返回 List.class,这里传入的 type 情况可能比较复杂,因此不能直接当做
* Class 去做判断。这个方法在判断类型是否为支持的类型时经常用到。
protected static Class<?> getRawType(Type type) {
return Utils.getRawType(type);
}
}
}
代码中做了较为详细的注释,简单来说,我们只需要实现 CallAdapter 类来提供具体的适配逻辑,并实现相应的 Factory,用来将当前的 CallAdapter注册到 Retrofit 当中,并在 Factory.get 方法中根据类型来返回当前的 CallAdapter即可。知道了这些,我们再来看 RxJavaCallAdapterFactory: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
47public final class RxJavaCallAdapterFactory extends CallAdapter.Factory {
... 请叫我省略君,为了省地方,一个都不放过! ...
public CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
//注意下面的代码主要是判断 returnType 是否为 RxJava 支持的类型
Class<?> rawType = getRawType(returnType);
String canonicalName = rawType.getCanonicalName();
boolean isSingle = "rx.Single".equals(canonicalName);
boolean isCompletable = "rx.Completable".equals(canonicalName);
if (rawType != Observable.class && !isSingle && !isCompletable) {
return null;
}
... 这里省略掉的代码主要是根据返回类型获取合适的 Adapter ...
return callAdapter;
}
... 我又来了,继续略去一些代码 ...
static final class SimpleCallAdapter implements CallAdapter<Observable<?>> {
private final Type responseType;
private final Scheduler scheduler;
SimpleCallAdapter(Type responseType, Scheduler scheduler) {
this.responseType = responseType;
this.scheduler = scheduler;
}
public Type responseType() {
return responseType;
}
public <R> Observable<R> adapt(Call<R> call) {
//在这里创建需作为返回值的 Observable 实例,并持有 call 实例
//可以想象得到,在 Observable.subscribe 触发时, call.execute 将会被调用
Observable<R> observable = Observable.create(new CallOnSubscribe<>(call))
.lift(OperatorMapResponseToBodyOrError.<R>instance());
if (scheduler != null) {
return observable.subscribeOn(scheduler);
}
return observable;
}
}
//... 略去一些代码 ...
}
RxJavaCallAdapterFactory 提供了不止一种 Adapter,但原理大同小异,有兴趣的读者可以自行参阅其源码。
至此,我们已经对 CallAdapter 的机制有了一个清晰的认识了。
前面我们已经介绍了很多东西了。。可,挖掘机专业的同学们,你们觉得这就够了么?当然是不够!
在前面我们曾试图简化文件上传接口的使用,尽管我们已经给出了相应的 File -> RequestBody 的 Converter,不过基于 Retrofit本身的限制,我们还是不能像直接构造 MultiPartBody.Part 那样来获得更多的灵活性。这时候该怎么办?当然是 Hack~~
首先明确我们的需求:
为此,我增加了一套完整的参数解析方案:
1 | public interface Converter<F, T> { |
需要注意的是,Retrofit 类当中也需要增加相应的方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public <F, T> Converter<F, T> arbitraryConverter(Type orignalType,
Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations) {
return nextArbitraryConverter(null, orignalType, convertedType, parameterAnnotations, methodAnnotations);
}
public <F, T> Converter<F, T> nextArbitraryConverter(Converter.Factory skipPast,
Type type, Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations) {
checkNotNull(type, "type == null");
checkNotNull(parameterAnnotations, "parameterAnnotations == null");
checkNotNull(methodAnnotations, "methodAnnotations == null");
int start = converterFactories.indexOf(skipPast) + 1;
for (int i = start, count = converterFactories.size(); i < count; i++) {
Converter.Factory factory = converterFactories.get(i);
Converter<?, ?> converter =
factory.arbitraryConverter(type, convertedType, parameterAnnotations, methodAnnotations, this);
if (converter != null) {
//noinspection unchecked
return (Converter<F, T>) converter;
}
}
return null;
}
1 | public class TypedFileMultiPartBodyConverterFactory extends Converter.Factory { |
1 | public class TypedFileMultiPartBodyConverter implements Converter<TypedFile, MultipartBody.Part> { |
1 | public class TypedFile { |
1 | ... |
1 | static final class TypedFileHandler extends ParameterHandler<TypedFile>{ |
1 | public interface FileUploadService { |
以及使用方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.println.net/")
.addConverterFactory(new TypedFileMultiPartBodyConverterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build();
FileUploadService service = retrofit.create(FileUploadService.class);
TypedFile typedFile = new TypedFile("aFile", filename);
String descriptionString = "This is a description";
RequestBody description =
RequestBody.create(
MediaType.parse("multipart/form-data"), descriptionString);
Call<ResponseBody> call = service.upload(description, typedFile);
call.enqueue(...);
至此,我们已经通过自己的双手,让 Retrofit 的点亮了自定义上传文件的技能,风骚等级更上一层楼!
我们在开发过程中,经常遇到服务端不稳定的情况,测试开发环境,这是难免的。于是我们需要能够模拟网络请求来调试我们的客户端逻辑,Retrofit 自然是支持这个功能的。
真是太贴心,Retrofit 提供了一个 MockServer 的功能,可以在几乎不改动客户端原有代码的前提下,实现接口数据返回的自定义,我们在自己的工程中增加下面的依赖:1
compile 'com.squareup.retrofit2:retrofit-mock:2.0.2
还是先让我们来看看官方 demo,首先定义了一个 GituHb api,好熟悉的感觉:1
2
3
4
5
6public interface GitHub {
"/repos/{owner}/{repo}/contributors") (
Call<List<Contributor>> contributors(
"owner") String owner, (
"repo") String repo); (
}
这就是我们要请求的接口了,怎么 Mock 呢?
1 | static final class MockGitHub implements GitHub { |
1 | // Create a very simple Retrofit adapter which points the GitHub API. |
1 | Call<List<Contributor>> contributors = gitHub.contributors(owner, repo); |
也就是说,我们完全可以自己造一个假的数据源,通过 Mock Server 来返回这些写数据。
那么问题来了,这其实并没有完全模拟网络请求的解析流程,如果我只能提供原始的 json 字符串,怎么通过 Retrofit 来实现 Mock Server?
时间已经不早啦,我就不猥琐发育了,直接推塔~
本文前面一直专注于介绍 Retrofit,很少提及 OkHttp,殊不知 OkHttp 有一套拦截器的机制,也就是说,我们可以任性的检查 Retrofit 即将发出或者正在发出的所有请求,并且篡改它。所以我们只需要找到我们想要的接口,定制自己的返回结果就好了,下面是一段示例: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
29OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new Interceptor() {
public Response intercept(Chain chain) throws IOException {
Response response = null;
if(BuildConfig.DEBUG && chain.request().url().uri().getPath().equals("/contributors")) {
//这里读取我们需要返回的 Json 字符串
String responseString = ...;
response = new Response.Builder()
.code(200)
.message(responseString)
.request(chain.request())
.protocol(Protocol.HTTP_1_0)
.body(ResponseBody.create(MediaType.parse("application/json"), responseString.getBytes()))
.addHeader("content-type", "application/json")
.build();
} else {
response = chain.proceed(chain.request());
}
return response;
}
}).build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
这样,我们就会拦截 contributors 这个 api 并定制其返回了。
Retrofit 是非常强大的,本文通过丰富的示例和对源码的挖掘,向大家展示了 Retrofit 自身强大的功能以及扩展性,就算它本身功能不能满足你的需求,你也可以很容易的进行改造,毕竟人家的代码真是写的漂亮啊。
]]>SensorManager.java
:从android4.1开始,把SensorManager定义为一个抽象类,定义了一些主要的方法,该类主要是应用层直接使用的类,提供给应用层的接口SystemSensorManager.java
:继承于SensorManager,客户端消息处理的实体,应用程序通过获取其实例,并注册监听接口,获取sensor数据。sensorEventListener
接口:用于注册监听的接口sensorThread
:是SystemSensorManager的一个内部类,开启一个新线程负责读取读取sensor数据,当注册了sensorEventListener接口的时候才会启动线程android_hardware_SensorManager.cpp
:负责与java层通信的JNI接口SensorManager.cpp
:sensor在Native层的客户端,负责与服务端SensorService.cpp的通信SenorEventQueue.cpp
:消息队列SensorService.cpp
:服务端数据处理中心SensorEventConnection
:从BnSensorEventConnection继承来,实现接口ISensorEventConnection的一些方法,ISensorEventConnection在SensorEventQueue会保存一个指针,指向调用服务接口创建的SensorEventConnection对象Bittube.cpp
:在这个类中创建了管道,用于服务端与客户端读写数据SensorDevice
:负责与HAL读取数据Sensor.h
是google为Sensor定义的Hal接口,单独提出去1 | SensorManager mSensorManager = |
系统开机启动的时候,会创建SystemSensorManager的实例,在其构造函数中,主要做了四件事情:
1 | public SystemSensorManager(Context context,Looper mainLooper) { |
1 | protected boolean registerListenerImpl(SensorEventListener listener, Sensor sensor, |
1 | boolean startLocked() { |
com_android_server_SystemServer.cpp
的android_server_SystemServer_init1()
方法,该方法又调用system_init.cpp中的system_init()
,在这里创建了SensorService的实例。1 | extern "C" status_t system_init() |
1 | SensorDevice::SensorDevice() |
这里主要做了三个工作:
再看看SensorService::onFirstRef()方法:
1 | void SensorService::onFirstRef() |
在这个方法中,主要做了4件事情:
registerSensor(new HardwareSensor(list[i]))
;
1 | void SensorService::registerSensor(SensorInterface* s) |
SensorService::threadLoop()
方法。在while循环中一直读取HAL层数据,再调用SensorEventConnection->sendEvents
将数据写到管道中。1 | bool SensorService::threadLoop() |
1 | typedef struct sensors_event_t { |
1 | typedef struct ASensorEvent { |
1 | class SensorService : |
1 | template<typename SERVICE> |
在前面的介绍中,SensorService服务的实例是在System_init.cpp中调用SensorService::instantiate()创建的,即调用了上面的instantiate()方法,接着调用了publish(),在该方法中,我们看到了new SensorService的实例,并且调用了defaultServiceManager::addService()将Sensor服务添加到了系统服务管理中,客户端可以通过defaultServiceManager:getService()获取到Sensor服务的实例。
继承BnSensorServer这个是sensor服务抽象接口类提供给客户端调用:
1 | class Sensor; |
1 | class ISensorEventConnection; |
1 | status_t SensorManager::assertStateLocked() const { |
new ListenerDelegate(SensorEventListener listener, Sensor sensor, Handler handler)
在这个构造函数中会创建一个Handler,它会在获取到Sensor数据的时候被调用。1 | mHandler = new Handler(looper) { |
当客户端第一次注册监听器的时候,就需要创建一个消息队列,也就是说,android在目前的实现中,只创建了一个消息队列,一个消息队列中有一个管道,用于服务端与客户断传送Sensor数据。
在SensorManager.cpp中的createEventQueue方法创建消息队列:
1 | sp<SensorEventQueue> SensorManager::createEventQueue() |
SensorEventConnection
连接接口,而一个消息队列中包含一个连接接口。创建连接接口:1 | sp<ISensorEventConnection> SensorService::createSensorEventConnection() |
1 | BitTube::BitTube() |
1 | status_t SensorService::SensorEventConnection::enableDisable( |
1 | bool SensorService::threadLoop() |
前面介绍过,在SensorService中,创建了一个线程不断从HAL层读取Sensor数据,就是在threadLoop方法中。
关键在与下面了一个for循环,其实是扫描有多少个客户端连接接口,然后就往没每个连接的管道中写数据
1 | status_t SensorService::SensorEventConnection::sendEvents( |
1 | ssize_t BitTube::write(void const* vaddr, size_t size) |
1 | ssize_t SensorEventQueue::read(ASensorEvent* events, size_t numEvents) |
1 | static ssize_t recvObjects(const sp<BitTube>& tube, |
Android6.0 系统内置对传感器的支持达26种,他们分别是:加速度传感器(accelerometer)、磁力传感器(magnetic field)、方向传感器(orientation)、陀螺仪(gyroscope)、环境光照传感器(light)、压力传感器(pressure)、温度传感器(temperature)和距离传感器(proximity)等。
Android实现传感器系统包括以下几个部分:
各部分之间架构图如下:
Google为Sensor提供了统一的HAL接口,不同的硬件厂商需要根据该接口来实现并完成具体的硬件抽象层。Android中Sensor的HAL接口定义在:hardware/libhardware/include/hardware/sensors.h
对传感器类型的定义:
1 |
传感器模块的定义结构体如下,该接口的定义实际上是对标准的硬件模块hw_module_t的一个扩展,增加了一个get_sensors_list
函数,用于获取传感器的列表。
1 | struct sensors_module_t { |
对任意一个sensor设备都会有一个sensor_t
结构体,其定义如下:
1 | struct sensor_t { |
每个传感器的数据由sensors_event_t
结构体表示,定义如下,其中,sensor为传感器的标志符,而不同的传感器则采用union方式来表示。
1 | typedef struct sensors_event_t { |
sensors_vec_t
结构体用来表示不同传感器的数据:
1 | typedef struct { |
Sensor设备结构体sensors_poll_device_t
,对标准硬件设备hw_device_t
结构体的扩展,主要完成读取底层数据,并将数据存储在struct sensors_poll_device_t
结构体中;poll函数用来获取底层数据,调用时将被阻塞定义如下:
1 | struct sensors_poll_device_t { |
控制设备打开/关闭结构体定义如下:
1 | static inline int sensors_open(const struct hw_module_t* module, |
open_sensors()
方法打开设备模块,再调用poll__activate()
对设备使能,然后调用poll__poll
读取数据。1 | $ git clone -b hexo git@github.com:way1989/way1989.github.io.git |
1 | $ hexo s --debug 然后打开`localhost:4000`就可以看见刚才发表的测试文章了 |
1 | $ hexo clean #清除网页缓存 |
hexo-generator-search
,在站点的根目录下执行以下命令:1 | $ npm install hexo-generator-search --save |
站点配置文件
,新增以下内容到任意位置:1 | search: |
1 | $ npm install hexo-deployer-git --save |
1 | $ sudo blkid /dev/sdb1 |
1 | /dev/sdb1: LABEL="SSD" UUID="861ada17-a2a2-4781-8ec7-5b93794adf9b" TYPE="ext4" |
1 | $ sudo vim /etc/fstab |
1 | UUID=861ada17-a2a2-4781-8ec7-5b93794adf9b /SSD ext4 defaults 1 2 |
1 | $ sudo mount -a |
EventBus is a publish/subscribe event bus optimized for Android.
Gradle:1
2gradle
compile 'org.greenrobot:eventbus:3.0.0'
download EventBus from Maven Central
1 | public class LoadEvent { |
1 | EventBus.getDefault().register(this); |
1 | EventBus.getDefault().unregister(this); |
1 | 0, sticky = false) (threadMode = ThreadMode.MAIN, priority = |
If true, delivers the most recent sticky event (posted with EventBus#postSticky(Object)) to this subscriber (if event available).
1 | EventBus.getDefault().post(new LoadEvent()); |
1 | public class MainActivity extends AppCompatActivity { |
1 | classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' |
1 | apply plugin: 'com.neenbedankt.android-apt' |
1 | apt 'org.greenrobot:eventbus-annotation-processor:3.0.1' |
1 | EventBus.builder().addIndex(new MySubscriberInfoIndex()).installDefaultEventBus(); |
1 | public static EventBus getDefault() { |
1 | public void register(Object subscriber) { |
1 | private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType; |
1 | private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) { |
1 | private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) { |
1 | public void post(Object event) { |
1 | private void postSingleEvent(Object event, PostingThreadState postingState) throws Error { |
1 | private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) { |
1 | private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) { |
1 | void invokeSubscriber(Subscription subscription, Object event) { |
1 | private final Map<Object, List<Class<?>>> typesBySubscriber; |
1 | private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType; |
1 | public class RxBus { |
1 | RxBus.getInstance().toObservable(ForceUpdateEvent.class) |
1 | RxBus.getInstance().post(new ForceUpdateEvent(parameter)); |
1 | public class ForceUpdateEvent { |
1 |
|
1 | $ openssl pkcs8 -in platform.pk8 -inform DER -outform PEM -out platform.priv.pem -nocrypt |
1 | $ openssl pkcs12 -export -in platform.x509.pem -inkey platform.priv.pem -out platform.pk12 -name brilliance |
1 | $ keytool -importkeystore -deststorepass android -destkeypass android -destkeystore platform.keystore -srckeystore platform.pk12 -srcstoretype PKCS12 -srcstorepass android -alias brilliance |
1 |
|
1 | public static Button sActionBarDownload; //title栏的下载 |
1 | private void sendFile(Activity activity) { |
Capturer是为Android平台设计的,集截屏、长截屏、录屏以及将录屏内容转换成GIF动态图
等多功能于一体的,小巧强悍的实用型工具软件。
确保Capturer已经正常启动,进入到任何需要截屏的应用,摇一摇启动Captain主菜单,点击顺时针两点钟位置的图标
开始截屏(长按会有提示)。
与系统长按电源键+音量键
截屏是一样的。
普通截屏演示范例:
确保Capturer已经正常启动,进入到任何需要录屏的应用,摇一摇启动Captain主菜单,点击顺时针四点钟位置的图标
开始录屏(长按会有提示)。
当屏幕上方录屏操作菜单后,还没真正开始录屏,可以点击左边的按钮
取消录屏,点击右边的按钮
切换录制视频的画质质量(全高清画质与屏幕分辨率一致,高清为屏幕分辨率的75%,标清为50%),点击中间的按钮
才开始真正的录屏。
倒计时3秒后,显示停止按钮并计时表示真正开始录屏了,可以通过点击停止按钮
随时结束录屏。
画质越高,录制的视频文件就越大,显示效果越好。
3秒倒计时可以在设置中取消。
停止按钮可以设置显示在通知栏中。
确保Capturer已经正常启动,进入到需要长截屏的应用,摇一摇启动Captain主菜单,点击顺时针六点钟位置的图标
开始长截屏(长按会有提示)。
当屏幕上方显示提示界面后,按照屏幕箭头的方向滚动与箭头长度相同的高度。
滚动完一次之后,点击一次上方的提示界面,再接着滚动一次,点击一次,如果想结束长截屏,直接点击两次即可。
滚动一次,点击一次,连点两次直接结束。
每次滚动的高度尽量与箭头长度相近,避免滚动过大导致长图拼接失败。
确保Capturer已经正常启动,进入到需要任意截屏的应用,摇一摇启动Captain主菜单,点击顺时针十点钟位置的图标
开始任意截屏(长按会有提示)。
当屏幕中显示提示界面,按照提示用手指圈选需要截取的部分,点击确定
按钮保存截图。
如果需要重新选取截屏区域,直接用手指圈选新的区域即可。
保存圈选区域内容需要点击确定
按钮。
选择新区域时,不需要做其他操作,直接在屏幕上重新圈选。
不圈选直接点击确定
按钮,将直接保存完整屏幕截图。
确保Capturer已经正常启动,进入到需要矩形区域截屏的应用,摇一摇启动Captain主菜单,点击顺时针十二点种位置的图标
开始矩形区域截屏(长按会有提示)。
当屏幕中显示提示界面,按照提示,调整矩形框的大小和拖动矩形框的位置来选择需要截屏的部分。
双击屏幕即可保存所选区域截屏内容。
通过矩形框四个角调整大小,通过触摸矩形框其他区域可以拖动。
双击即可保存矩形框区域内的截屏内容。
进入Capturer,选择视频TAB页,点击任何需要转换的视频,然后再点击播放按钮开始播放编辑视频。
通过底部拖动按钮选取需要转换视频的时间。
点击生成GIF
按钮即可弹出选择GIF画质的对话框,选择一个画质点击确定即可开始转换。
第一次开始转换时,需要下载一个FFmpeg
动态库。
选取转换的时间只能精确到秒,因此可能生成GIF图前后会存在少许误差。
精细画质宽480,帧率12,标准画质360,帧率10,压缩画质宽240,帧率8,为减少用户操作,暂时固定这几种画质。
画质越高,选择视频时间越长,生成GIF文件也就越大。