告白

告白

像我这么一个如此理性的人,竟然会喜欢一个人这么长时间。

五年的时间。你就这么出现在我的世界里,悄无声息的融入到我的生活。以为你一直能陪伴到最后,从来不曾想你也会离去。我能理解,但我不能接受,也不想接受。

不能想象你身上的球衣不再是白色,不能想象伯纳乌的7号球衣会有新的主人。而我甚至都还没有亲眼到现场看过白衣飘飘的你在球场奔跑的样子。

因为大罗喜欢上足球。而你是我的足球初恋,到现在都还是觉得有些奇怪,为什么在当时巴萨如日中天,梅西势不可挡的时候在周围一票巴萨球迷的情况下还是选择了你。我看球晚,没见过伯纳乌王子在球场上的表现。从我看球第一眼,你就是伯纳乌的主人。我是幸运的,从13/14赛季开始,有幸见证了足球历史上最传奇的一段岁月,五年四冠,欧洲之王。

刚开始喜欢看你带球狂奔,花式过人,暴力头槌,无解电梯球。不过如果当时有人说,你会反超梅西,估计是没有人会相信的。14/15那个赛季,四大皆空,网上全是黑子,甚至有段时间都不敢打开新闻。被带节奏,认为你浪射,浪费机会,不支持队友……直到踢狼堡的第二回合,踢那个任意球的时候那个眼神,这辈子都不会忘,从那开始你好像就变了个人,我也从那次决定再不黑你,很庆幸那场球看了直播,还有客场踢拜仁4:0的那种,完美的表现。然后就是16年欧洲杯的时候,可以说是奇迹吧,欧洲球王就是你的完美称号。从此,你又转型,减重,减少持球,做纯粹的“射门员”,你和齐祖互相成全,三连冠,前无古人。之前一直以为自己是队迷,到现在才发现我是你的人迷。

你就是最完美的球员。

希望明年皇马尤文再次出现在欧冠决赛的舞台,120分钟内4:4,你上演大四喜,然后结果就交给点球大战吧,最完美的结局。

1个传奇,2夺联赛,3连欧冠,4座金球,5子登科,6双欧靴, 7号球衣,8次一阵, 9年时光,10分不舍。

Cristiano Ronaldo

Cristiano
Cristiano
Cristiano
Cristiano

Android 动态权限申请

简介

权限列表

Android6.0以上把权限分为普通权限和危险权限,所以危险权限是需要动态申请,给予用户提示的。

看到上面的 permissions,会发现一个问题,危险权限都是一组一组的。

分组对权限机制的申请是有一定影响的。例如app运行在android 6.x的机器上,对于授权机制是这样的。如果你申请某个危险的权限,假设你的app早已被用户授权了同一组的某个危险权限,那么系统会立即授权,而不需要用户去点击授权。比如你的app对 READ_CONTACTS 已经授权了,当你的app申请 WRITE_CONTACTS 时,系统会直接授权通过。

此外,对于申请时的弹窗上面的文本说明也是对整个权限组的说明,而不是单个权限。

动态权限申请

1.首先要保证在AndroidManifest中写明需要的权限。

1
2
3
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.CAMERA"/>

2.权限申请示例 以获取定位权限为例。

  1. 点击按钮,检查并申请权限
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    if (Build.VERSION.SDK_INT >23) {
    if (ContextCompat.checkSelfPermission(MainActivity.this,
    Manifest.permission.ACCESS_COARSE_LOCATION)
    == PackageManager.PERMISSION_GRANTED) {
    //授予权限
    getLoation();
    }else{
    //未获得权限
    requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}
    ,REQUEST_CODE_LOCATION);
    }
    }
    }
    });

如果有权限,执行获取位置逻辑,如果没权限,则进行请求权限。

  1. 权限申请结果回调
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    if (requestCode == REQUEST_CODE_LOCATION){
    if (grantResults[0] == PackageManager.PERMISSION_GRANTED){
    getLoation();
    } else{
    if (shouldShowRequestPermissionRationale( Manifest.permission.ACCESS_COARSE_LOCATION)){
    new AlertDialog.Builder(this)
    .setMessage("申请定位权限,才能为你推送更准确的信息")
    .setPositiveButton("确定", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
    //申请定位权限
    requestPermissions(MainActivity.this,
    new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, REQUEST_CODE_LOCATION);
    }
    }).show();
    }
    }
    return;
    }
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

如果同意,执行获取位置逻辑,如果拒绝,重写 shouldShowRequestPermissionRationale 方法,返回 true,向用户弹窗给出一个获取权限的提示,点击后再次申请权限。

1
2
3
4
5
6
7
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) {
if (permission.equals(Manifest.permission.ACCESS_COARSE_LOCATION) ) {
return true;
} else {
return super.shouldShowRequestPermissionRationale(permission);
}
}

重写shouldShowRequestPermissionRationale,在申请位置权限时,返回true,给用户解释。

完整代码

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class PermissionActivity extends AppCompatActivity implements View.OnClickListener {

private static final String TAG = "PermissionActivity";
public static final int REQUEST_CAMERA = 1;
private ConstraintLayout constraintLayout;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_permission);

initView();
}

private void initView() {
constraintLayout = findViewById(R.id.mainLayout);
findViewById(R.id.btn_camera).setOnClickListener(this);
findViewById(R.id.btn_contact).setOnClickListener(this);
}

@RequiresApi(api = Build.VERSION_CODES.M)
private boolean checkPermission(String permission) {
int i = checkSelfPermission(permission);
return i != PackageManager.PERMISSION_DENIED;
}

private void showSnackbar(String message) {
Snackbar.make(constraintLayout, message, Snackbar.LENGTH_SHORT).show();
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_camera:
showCamera();
break;
case R.id.btn_contact:
break;
}
}

private void showCamera() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkPermission(Manifest.permission.CAMERA)) {
showSnackbar("已授权,可以打开");
} else {
if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
Snackbar.make(constraintLayout, "Camera permission is needed", Snackbar.LENGTH_SHORT)
.setAction("确定", new View.OnClickListener() {
@Override
public void onClick(View v) {
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
}
}).show();
} else {
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
}
}
} else {
showSnackbar("可以直接打开");
}
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CAMERA:
if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showSnackbar("已授权");
} else {
showSnackbar("拒绝授权");
}
}
break;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

另外,在看一些 权限申请 的三方库的时候,发现了一件很有意思的事情。通常代码很难追踪到 Activity 的生命周期,一些库就新建一个 Fragment 在其生命周期的各个回调方法里去调用相应的方法。很棒。

参考文章

Lottie 详解

简介

Lottie 是 Airbnb 开源的一个面向 iOS、Android、React Native 的动画库,能分析 Adobe After Effects 导出的动画,并且能让原生 App 像使用静态素材一样使用这些动画,完美实现动画效果。

现在使用各平台的 native 代码实现一套复杂的动画是一件很困难并且耗时的事,我们需要为不同尺寸的屏幕加载不同的素材资源,还需要写大量难维护的代码,而 Lottie 可以做到同一个动画文件在不同平台上实现相同的效果,极大减少开发时间,实现不同的动画,只需要设置不同的动画文件即可,极大减少开发和维护成本。

官方效果图:

官方效果图

使用

Lottie 支持多平台,使用同一个 JSON 动画文件,可在不同平台实现相同的效果。其中,Android 通过 Airbnb 的开源项目 lottie-android 实现,最低支持 API 16。

引入依赖

implementation 'com.airbnb.android:lottie:2.5.4'

添加 Adobe After Effects 导出的动画文件

Lottie 默认读取 Assets 中的文件,我们需要把动画文件 react.json 保存在 app/src/main/assets 文件里。在 官网 可以免费下载动画文件。

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
// react.json
{
"v": "4.6.0",
"fr": 29.9700012207031,
"ip": 0,
"op": 141.000005743048,
"w": 800,
"h": 800,
"ddd": 0,
"assets": [ ],
"layers": [
{
"ddd": 0,
"ind": 0,
"ty": 4,
"nm": "center_circle",
"ks": {...},
"ao": 0,
"shapes": [...],
"ip": 0,
"op": 900.000036657751,
"st": 0,
"bm": 0,
"sr": 1
},
{...},
{...},
{...}
]
}

使用Lottie

在布局文件中直接添加 Lottie 的 LottieAnimationView 控件,即可在界面显示 React logo 动画效果

1
2
3
4
5
6
7
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="react.json"
app:lottie_loop="true"
app:lottie_autoPlay="true" />

属性说明:

  • app:lottie_fileName: assets 文件夹下对应的动画文件的文件名
  • app:lottie_loop: 是否循环播放动画
  • app:lottie_autoPlay: 是否自动播放动画
  • app:lottie_cacheStrategy: 设置缓存策略。默认为none,可选值有strong和weak
  • app:lottie_colorFilter: 设置背景颜色
  • app:lottie_progress: 设置动画播放进度
  • app:lottie_imageAssetsFolder: 动画所需图片资源在assets中的绝对路径。如果没有图片资源,可以省略

代码使用:

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
51
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view);

animationView.playAnimation(); //播放动画

animationView.setAnimation("data1.json", CacheStrategy.Strong); // 设置动画文件以及缓存策略

PorterDuffColorFilter colorFilter = new PorterDuffColorFilter(ContextCompat.getColor(this,android.R.color.holo_blue_light), PorterDuff.Mode.ADD);
animationView.addColorFilter(colorFilter); // 设置背景颜色

animationView.loop(true); // 循环播放

animationView.setProgress(0.5f); // 设置动画播放进度。需要注意值是float型。

// 监听动画的状态
animationView.addAnimatorListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
// 开始
}

@Override
public void onAnimationEnd(Animator animation) {
// 结束
}

@Override
public void onAnimationCancel(Animator animation) {
// 取消
}

@Override
public void onAnimationRepeat(Animator animation) {
// 重复
}
});

// 动态监听动画演示过程,可以动态获取动画进度
animationView.addAnimatorUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {

}
});

animationView.pauseAnimation(); // 暂停动画

animationView.isAnimating() // 是否正在播放动画

animationView.resumeAnimation() // 恢复动画

animationView.cancelAnimation() // 取消动画

使用小技巧

加载SDCard动画文件

1
2
3
4
5
6
7
8
9
10
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new FileReader(new File(JSON_PATH + "react.json")));
String content = null;
while ((content = bufferedReader.readLine()) != null){
stringBuilder.append(content);
}
JSONObject jsonObject = new JSONObject(stringBuilder.toString());
animationView.setAnimation(jsonObject);
animationView.loop(true);
animationView.playAnimation();

加载SDCard图片

1
2
3
4
5
6
7
8
9
10
11
12
animationView.setImageAssetDelegate(new ImageAssetDelegate() {
@Override
public Bitmap fetchBitmap(LottieImageAsset asset) {
try {
FileInputStream fileInputStream = new FileInputStream(IMAGE_PATH + asset.getFileName());
return BitmapFactory.decodeStream(fileInputStream); ///把流转化为Bitmap图片
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
});

加载SDCard字体

1
2
3
4
5
6
animationView.setFontAssetDelegate(new FontAssetDelegate(){
public Typeface fetchFont(String fontFamily) {
Typeface customFont = Typeface.createFromFile(FONT_PATH + fontFamily);
return customFont;
}
});

缓存动画

1
2
3
4
5
/**
*Lottie内部有两个缓存map(强引用缓存,弱引用缓存),在动画文件加载完成后会根据设置的缓存策略缓存动画,方便下次使用。
*/
animationView.setAnimation(animation, LottieAnimationView.CacheStrategy.Strong); //强缓存
animationView.setAnimation(animation, LottieAnimationView.CacheStrategy.Weak); //弱缓存

Lottie实现原理

Lottie 的使用的资源是需要先通过 bodymovin( bodymovin 插件本身是用于网页上呈现各种AE效果的一个开源库) 将 Adobe After Effects (AE) 生成的aep动画工程文件转换为通用的json格式描述文件。Lottie则负责解析动画的数据,计算每个动画在某个时间点的状态,准确地绘制到屏幕上。

Lottie 对外通过控件 LottieAnimationView 暴露接口,控制动画。
LottieAnimationView 继承自 ImageView,通过当前时间绘制 canvas 显示到界面上。这里有两个关键类:LottieComposition 负责解析json描述文件,把json内容转成Java数据对象;LottieDrawable负责绘制,把LottieComposition转成的数据对象绘制成drawable显示到View上。顺序如下:

顺序

探究更多原理解析内容请在 Lottie–让动画如此简单 查看。

性能

内容来自 腾讯音乐技术团队 。

官方说明

  • 如果没有 mask 和 mattes,那么性能和内存非常好,没有 bitmap 创建,大部分操作都是简单的 cavas 绘制。
  • 如果存在 mattes,将会创建2~3个 bitmap。bitmap 在动画加载到 window 时被创建,被 window 删除时回收。所以不宜在 RecyclerView 中使用包涵 mattes 或者 mask 的动画,否则会引起 bitmap 抖动。除了内存抖动,mattes和mask中必要的 bitmap.eraseColor() 和 canvas.drawBitmap() 也会降低动画性能。对于简单的动画,在实际使用时性能不太明显。
  • 如果在列表中使用动画,推荐使用缓存 LottieAnimationView.setAnimation(String, CacheStrategy)

属性动画和Lottie动画对比

以下性能对比是以K歌内单个礼物动画效果

- 属性动画 lottie使用硬件加速 lottie未使用硬件加速
帧率
内存
cpu

总结

Lottie使用简单,易于上手,非常值得尝试。

优势

  • 开发效率高—代码实现简单,更换动画方便,易于调试和维护。
  • 数据源多样性—可从assets,sdcard,网络加载动画资源,能做到不发版本,动态更新
  • 跨平台—设计稿导出一份动画描述文件,android,ios,react native通用

劣势

  • 性能不够好—某些动画特效,内存和性能不够好;
  • 相对于属性动画,在展示大动画时,帧率较低

参考文章

RecyclerView IndexOutOfBoundsException

前言

最近使用 RecyclerView 的 notifyItemInsertednotifyItemRemoved 的时候,经常遇到这个异常。

1
2
3
4
5
6
7
8
9
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{2064e5c6 position=2 id=-1, oldPos=2, pLpos:-1 scrap [attachedScrap] tmpDetached no parent}
at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:4505)
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4636)
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4617)
at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:1994)
at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1390)
at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1353)
at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:574)
...

查了一些资料,大多是说因为数据源改变了,但是 adapter 却没有及时调用 notifyItemRangeChanged

解决方案

方法一

自己写一个继承 LinearLayoutManager 的包装类,在 onLayoutChildren() 方法里 try-catch 捕获该异常。然后给 recyclerView 设置该 LayoutManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class WrapContentLinearLayoutManager extends LinearLayoutManager {
//... constructor
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
super.onLayoutChildren(recycler, state);
} catch (IndexOutOfBoundsException e) {
Log.e("probe", "meet a IOOBE in RecyclerView");
e.printStackTrace();
}
}
}
...

RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view);

recyclerView.setLayoutManager(new WrapContentLinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false));

方法二

在进行数据移除和数据增加时,务必要保证RecyclerView的Adapter中的数据集和移除/添加等操作后的数据集保持一致!

这里,前者是在该Adapter内部,不妨叫做内部数据集,后者是开发人员传过给Adapter的,不妨叫外部数据集。更新RecyclerView数据时,需要保证外部数据集和内部数据集实时保持一致。每一次对外部数据集做改动时,都需要紧接着主动对外部数据集和内部数据集做一次同步操作

外部数据集同步到内部数据集,使用如下的方法:

  • notifyItemRangeRemoved();
  • notifyItemRangeInserted();
  • notifyItemRangeChanged();
  • notifyDataSetChanged();

这里对 notifyDataSetChange() 做些说明:

使用该方法的更新内部数据集,没有默认的动画效果,同时更新数据的效率页不如上面的方法,官方不推荐使用这种方式更新数据集。

方法三

使用notifyDataSetChanged同步外部数据集和内部数据集。该方法简单,但是失去了动画效果,并且更新数据的性能低。

另外,如果对外部数据集做了二次以上的操作,却只调用notifyDataSetChanged同步一次,也很有可能会报上文所示的错误。

参考文章:

可是过往便是过往

我终究还是要走的,纵然我可以舍得,纵然我舍不得。

我的记忆里,有这里的时光,有这里风吹过的声音,有许许多多人的笑脸和哭泣,那我对于这段 时光而言,又算什么呢。

大概一切只是一个名字。

对于一个终究要说再见的人而言,忘掉这段记忆对你而言,很容易,只要你忘记我的名字。

不过谢谢你,谢谢你的所有,谢谢你。

可是啊,过往便是过往,不可磨灭啊。

可是啊,可是过往也不是过往,而我要怎么忘记你呢。

对你的一切视而不见。假装从来不知道你离开,也假装你明天就会回来,假装自己是一个哑巴,说不出任何话,也发不出任何声音。假装站在你面前,继续夸夸其谈我的优秀,假装关于你的一切我从未在意。

你何不忘记她,也许我从未对你说过很多话,但我绝不会不告而别,绝不会枉顾你的心意,绝不会假装看不到你难过,绝不会在每一个有情节的段落里,走漏一点点关于你的风声。哪怕到老,我会像一个小偷瞒天过海,藏着你和我之前所有的秘密,一个人,悄悄看着他们。

我在另外一个地方,听到有两个声音同时在跟我说话。

一个声音牵挂着我,另外一个声音在和他争吵。

我在不知名的地方醒来,看见那些潮湿而温暖的过去,被陡然的隐藏在地平线下。

我在不知名的地方,用回忆封印,借助冰冷的事物前行,春夏秋冬疏忽而过。

而我的马蹄,大概永远都在美丽的错过。

也许,我不是归人,只是一个过客。

那个所谓的过客,你能听得见我的声音吗?

我听得见。

她听不见!

我猜,也许是我们约定的时针除了差错。

也许在你出现的上一秒,有人加快了脚步。

他只是无视了时间,悄悄把踪迹擦掉。

否则我不会一直原地,苦苦等待你的下落。

也许我和你的齿轮,终究无法再邂逅。

可能她在另一个世界苏醒,已经更改了灵魂。

像永远等不到黄昏黎明的日与夜。

像,空荡码头,永远等着船只的水手。

像,落在深海上,寂静无声的雨点。

我赤手空拳,就像个无赖。

但那又怎样呢。

你以为她独自处在沙漠,你就想做那片遥远绿洲吗?

我啊,我喜欢我没有的东西,而你是那么的遥远。

是啊,而我们竟还要不断地赶路,各奔东西。

远或近,她在意吗?

但你知道吗,我曾经以为我和你的名字总会靠在一起。

可谁都要不断接受别离,重逢与错过。

她并不是为了你一个人而存在。

但你知道吗?我不在乎你是往左或是往右。

你以为谁都很清楚自己的方向。

所以你认定了吧,她的方向就是你的方向。

但你知道吗,我就想一个木偶,扯线的人已经不知去向。

凭什么归咎于我!

所以,你变成了一个哑巴。

告诉我,我用什么才能留住你。

不如,忘记我。

她甚至没有给你挽留的机会。

告诉我,是不是一定要无疾而终,来吊足我的胃口。

不告而别,也许是我的婉转。

是不是非得一记响亮的结果。

你满意了吧?

你又怎么知道?

可能疯了吧。

但是你的情绪有一点点是来自于我,我就是成功的。

这就是你对成功的定义吗?

也许无动于衷,是你的方式。而她,只想奉还给你。

你不敢跟我正面迎战吗?因为我永远会赢你。

我是不想变得和你一样幼稚。

在她的世界中,载着过去的版图里,也许有你。

但愿。

我们就此作别,拥有不一样的路途和黄昏。

此生如一。

一心一意。

再见了。

你们会重逢吗?

一心。

再见了。

一切安好。

顾一心。

再见了。

健康幸福。

唉,你听得见吗?

原来,我听不见。

是的。她听得见。

如此,也好。

可是啊,顾一心,要是有一天,我能再见到你,我想对你说的,不是刚刚那些话。

你站在校门口,对我说,嗨,毕十三,我从美国回来看你了。

其实我想对你说,我杜撰过很多和你再相遇的版本,但那些都不是我真的会对你说的。而我想对你说什么呢?

顾一心,我真的好讨厌你,我讨厌你那么喜欢另一个男生,我讨厌你无视我的态度,我讨厌你的不辞而别,我讨厌你忘记我,我讨厌你甚至不曾记得我。

我讨厌你,我真的好讨厌你。

你呢?

你还讨厌我么?

这是个喧嚣的世界
我从未觉得安静过
他的繁荣
他的昌盛
带给人们却只是更多的疲倦
更多的抱怨

于是我捂住双耳
不去听他的疲倦
不去听他的昌盛
不去听他的繁荣
也不去听他的抱怨

于是我以为我的世界安静了

只是这世上总有那么一人,
哪怕她不曾对我讲过一言一语,
但我却听得到她的声音

Glide 源码解析 一

前言

在众多的图片加载框架中,Glide 是 Google 推荐的,并在自家的项目中大量使用的一个非常强大的框架,专注于平滑滚动,并且还提供 Gif,本地 Vedio 首帧的解码和显示。Glide 提供了非常便捷的链式调用接口,以及丰富的拓展和自定义功能,开发者可以非常简单地对框架进行配置和图片再加工。

如今 Gilde 已经更新到4.x,了解其源码对更好的使用 Glide ,以及学习相关的图片处理技术,学习更优雅的编码会有很大的帮助。

不得不说,Glide 整个框架的极其复杂的,特别是在对资源的转换和解码过程中,涉及了许多的嵌套循环,同时也使用了大量的工厂模式用于生产转换模块,编码模块,解码模块等,笔者在阅读过程中,多次迷失在茫茫的代码流中。

为此,萌生了将对 Glide 的理解记录成文的想法,借以理清思路。

那么接下来,我们就先看看 Glide 是如何进行框架初始化的。

探究

Glide.with发生了什么?

1. Glide单例的加载

使用过 Glide 的都知道,调用 Glide 加载一张图片时,第一句代码便是 Glide.with(this),这里肯定就是 Glide 的入口了,通过这句代码,Glide 开始了“漫漫的”初始化之路。

Glide 重载了多个 with 的方法,分别用于不同的情境下使用,我们看其中最常用的在 Activity 中调用的方法,即

1
2
3
4
@NonNull
public static RequestManager with(@NonNull Activity activity) {
return getRetriever(activity).get(activity);
}

首先,跟进 getRetriever(activity)

1
2
3
4
5
6
7
8
9
10
11
@NonNull
private static RequestManagerRetriever getRetriever(@Nullable Context context) {
// Context could be null for other reasons (ie the user passes in null), but in practice it will
// only occur due to errors with the Fragment lifecycle.
Preconditions.checkNotNull(
context,
"You cannot start a load on a not yet attached View or a Fragment where getActivity() "
+ "returns null (which usually occurs when getActivity() is called before the Fragment "
+ "is attached or after the Fragment is destroyed).");
return Glide.get(context).getRequestManagerRetriever();
}

这里首先检查了 context 是否为空,如果为 null,抛出异常。

我们重点来看 Glide.get(context)

1
2
3
4
5
6
7
8
9
10
11
12
@NonNull
public static Glide get(@NonNull Context context) {
if (glide == null) {
synchronized (Glide.class) {
if (glide == null) {
checkAndInitializeGlide(context);
}
}
}

return glide;
}

这里是一个典型的双检锁单例模式。

继续跟进 checkAndInitialzeGlide(context)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void checkAndInitializeGlide(@NonNull Context context) {
// In the thread running initGlide(), one or more classes may call Glide.get(context).
// Without this check, those calls could trigger infinite recursion.
if (isInitializing) {
throw new IllegalStateException("You cannot call Glide.get() in registerComponents(),"
+ " use the provided Glide instance instead");
}
isInitializing = true;
initializeGlide(context);
isInitializing = false;
}

private static void initializeGlide(@NonNull Context context) {
initializeGlide(context, new GlideBuilder());
}
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
51
52
@SuppressWarnings("deprecation")
private static void initializeGlide(@NonNull Context context, @NonNull GlideBuilder builder) {
Context applicationContext = context.getApplicationContext();
// 获取注解的 module
GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules();
List<com.bumptech.glide.module.GlideModule> manifestModules = Collections.emptyList();
if (annotationGeneratedModule == null || annotationGeneratedModule.isManifestParsingEnabled()) {
// AndroidMenifest.xml 中注册的 module
manifestModules = new ManifestParser(applicationContext).parse();
}

// 移除相同的GlideModule
if (annotationGeneratedModule != null
&& !annotationGeneratedModule.getExcludedModuleClasses().isEmpty()) {
Set<Class<?>> excludedModuleClasses =
annotationGeneratedModule.getExcludedModuleClasses();
Iterator<com.bumptech.glide.module.GlideModule> iterator = manifestModules.iterator();
while (iterator.hasNext()) {
com.bumptech.glide.module.GlideModule current = iterator.next();
if (!excludedModuleClasses.contains(current.getClass())) {
continue;
}
iterator.remove();
}
}

if (Log.isLoggable(TAG, Log.DEBUG)) {
for (com.bumptech.glide.module.GlideModule glideModule : manifestModules) {
Log.d(TAG, "Discovered GlideModule from manifest: " + glideModule.getClass());
}
}

RequestManagerRetriever.RequestManagerFactory factory =
annotationGeneratedModule != null
? annotationGeneratedModule.getRequestManagerFactory() : null;
builder.setRequestManagerFactory(factory);
for (com.bumptech.glide.module.GlideModule module : manifestModules) {
module.applyOptions(applicationContext, builder);
}
if (annotationGeneratedModule != null) {
annotationGeneratedModule.applyOptions(applicationContext, builder);
}
Glide glide = builder.build(applicationContext);
for (com.bumptech.glide.module.GlideModule module : manifestModules) {
module.registerComponents(applicationContext, glide, glide.registry);
}
if (annotationGeneratedModule != null) {
annotationGeneratedModule.registerComponents(applicationContext, glide, glide.registry);
}
applicationContext.registerComponentCallbacks(glide);
Glide.glide = glide;
}

留意最后将初始化得到的glide赋值给了Glide.glide的单例。

接下里就来看看在这初始化方法中,Glide都加载了哪些配置。

2. GlideModule配置加载

在使用 Glide 的时候,我们都会有一些想要设置的系统级配置,如设置缓存的存储位置,缓存区的大小,网络加载模块等等,那么我们通常就是使用 GldieModule 进行配置。在 Glide3.x 中,我们首先会定义一个继承于 GlideModule 的类,然后在项目的 AndroidMenifest.xml 中进行指定:

1
2
<meta-data android:name="com.test.GlideConfiguration"
android:value="GlideModule"/>

而在 Glide4 中,提供另外一个配置的模式,那就是注解,并且不再继承 GlideModule ,而是继承 AppGlideModule 和 LibraryGlideModule,分别对应 Application 和 Library,使用 @GlideModule 注解进行标记。而 Glide3.x 中的配置方式已经建议放弃使用。示例如下:

1
2
3
4
5
6
7
8
@GlideModule
public class GlideConfiguration extends AppGlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
//设置缓存到外部存储器
builder.setDiskCache(new ExternalPreferredCacheDiskCacheFactory(context));
}
}

Glide 是如何对 GlideModule 的配置进行初始化的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Context applicationContext = context.getApplicationContext();
GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules();

List<com.bumptech.glide.module.GlideModule> manifestModules = Collections.emptyList();
if (annotationGeneratedModule == null || annotationGeneratedModule.isManifestParsingEnabled()) {
manifestModules = new ManifestParser(applicationContext).parse();
}

if (annotationGeneratedModule != null
&& !annotationGeneratedModule.getExcludedModuleClasses().isEmpty()) {
Set<Class<?>> excludedModuleClasses =
annotationGeneratedModule.getExcludedModuleClasses();
Iterator<com.bumptech.glide.module.GlideModule> iterator = manifestModules.iterator();
while (iterator.hasNext()) {
com.bumptech.glide.module.GlideModule current = iterator.next();
if (!excludedModuleClasses.contains(current.getClass())) {
continue;
}
iterator.remove();
}
}

第二行代码中,getAnnotationGeneratedGlideModules() 会获取 Glide 注解自动生产的一个 Glide 的 Module 配置器。如下:

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
@Nullable
@SuppressWarnings({"unchecked", "deprecation", "TryWithIdenticalCatches"})
private static GeneratedAppGlideModule getAnnotationGeneratedGlideModules() {
GeneratedAppGlideModule result = null;
try {
Class<GeneratedAppGlideModule> clazz =
(Class<GeneratedAppGlideModule>)
Class.forName("com.bumptech.glide.GeneratedAppGlideModuleImpl");
result = clazz.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Failed to find GeneratedAppGlideModule. You should include an"
+ " annotationProcessor compile dependency on com.github.bumptech.glide:compiler"
+ " in your application and a @GlideModule annotated AppGlideModule implementation or"
+ " LibraryGlideModules will be silently ignored");
}
// These exceptions can't be squashed across all versions of Android.
} catch (InstantiationException e) {
throwIncorrectGlideModule(e);
} catch (IllegalAccessException e) {
throwIncorrectGlideModule(e);
} catch (NoSuchMethodException e) {
throwIncorrectGlideModule(e);
} catch (InvocationTargetException e) {
throwIncorrectGlideModule(e);
}
return result;
}

其中 com.bumptech.glide.GeneratedAppGlideModuleImpl 是在编译时由 Glide 生成的一个类,主要用于过滤不必要的 GlideModule ,以及提供一个请求检索器工厂,这个后面会讲到。

接下生成一个 Manifest 解析器 ManifestParser ,用于获取配置的 GlideModule ,并存放在 manifestModules 中。然后是一个判断

1
2
3
4
if (annotationGeneratedModule != null
&& !annotationGeneratedModule.getExcludedModuleClasses().isEmpty()) {
......
}

如果条件成立,即编译时自动生成的类中,包含了需要排除的 GlideModule ,逐个将其移除。

接着以上代码,Glide 将逐个调用剩下的 GlideModule ,并回调 applyOptionsregisterComponents 接口,这时,用户配置的 GlideModule 就会被调用,同时用户设置的参数也就被配置到 Glide 中。

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
RequestManagerRetriever.RequestManagerFactory factory =
annotationGeneratedModule != null
? annotationGeneratedModule.getRequestManagerFactory() : null;
builder.setRequestManagerFactory(factory);

// 逐个回调用户配置的 GlideModule
for (com.bumptech.glide.module.GlideModule module : manifestModules) {
module.applyOptions(applicationContext, builder);
}

// 回调注释标记的 AppGlideModule
if (annotationGeneratedModule != null) {
annotationGeneratedModule.applyOptions(applicationContext, builder);
}

// 构造 Glide 实例
Glide glide = builder.build(applicationContext);
for (com.bumptech.glide.module.GlideModule module : manifestModules) {
module.registerComponents(applicationContext, glide, glide.registry);
}
if (annotationGeneratedModule != null) {
annotationGeneratedModule.registerComponents(applicationContext, glide, glide.registry);
}
applicationContext.registerComponentCallbacks(glide);
Glide.glide = glide;

在以上代码中,发现一句代码,在回调 registerComponents 前,首先构建了 glide 的实例。

1
Glide glide = builder.build(applicationContext);

3. GlideBuilder构建Glide单例

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
@NonNull
Glide build(@NonNull Context context) {

/*
* 建立资源加载请求器,实际上是一个线程池,
* 用于加载 URL 或者本地资源,即加载源数据
*/
if (sourceExecutor == null) {
sourceExecutor = GlideExecutor.newSourceExecutor();
}

/*
* 建立本地缓存资源加载请求器,也是一个线程池
* 用于加载缓存在磁盘中的数据
* 并且这个线程池不能用于加载 url 网络数据
*/
if (diskCacheExecutor == null) {
diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();
}

/*
* 建立动画加载请求器,也是线程池
*/
if (animationExecutor == null) {
animationExecutor = GlideExecutor.newAnimationExecutor();
}

/*
* 内存计算器
*/
if (memorySizeCalculator == null) {
memorySizeCalculator = new MemorySizeCalculator.Builder(context).build();
}

/*
* 网络链接状态检测器工厂,
* 用于监听网络状态
*/
if (connectivityMonitorFactory == null) {
connectivityMonitorFactory = new DefaultConnectivityMonitorFactory();
}

/*
* 建立 bitmap 资源缓存池,
* 该缓存主要用于 bitmap 资源的缓存和回收,
* 避免由于大量创建和回收 bitmap 导致内存抖动
*/
if (bitmapPool == null) {
int size = memorySizeCalculator.getBitmapPoolSize();
if (size > 0) {
bitmapPool = new LruBitmapPool(size);
} else {
bitmapPool = new BitmapPoolAdapter();
}
}

/*
* 数组资源缓存池
*/
if (arrayPool == null) {
arrayPool = new LruArrayPool(memorySizeCalculator.getArrayPoolSizeInBytes());
}


/*
* 内存缓存,用于缓存完成加载和显示的图片数据资源
*/
if (memoryCache == null) {
memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
}

/*
* 本地磁盘缓存器,默认为存储在 app 内部私密目录
*/
if (diskCacheFactory == null) {
diskCacheFactory = new InternalCacheDiskCacheFactory(context);
}


/*
* 创建图片加载引擎,用于执行图片加载请求驱动
*/
if (engine == null) {
engine =
new Engine(
memoryCache,
diskCacheFactory,
diskCacheExecutor,
sourceExecutor,
GlideExecutor.newUnlimitedSourceExecutor(),
GlideExecutor.newAnimationExecutor(),
isActiveResourceRetentionAllowed);
}

/*
* 建立请求索引器
*/
RequestManagerRetriever requestManagerRetriever =
new RequestManagerRetriever(requestManagerFactory);

/*
* 创建 Glide
*/
return new Glide(
context,
engine,
memoryCache,
bitmapPool,
arrayPool,
requestManagerRetriever,
connectivityMonitorFactory,
logLevel,
defaultRequestOptions.lock(),
defaultTransitionOptions);
}

通过以上一系列工具的新建,Glide 建立了资源请求线程池,本地缓存加载线程池,动画线程池,内存缓存器,磁盘缓存工具等等,接着构造了 Engine 数据加载引擎,最后再将 Engine 注入 Glide ,构建 Glide 。

其中还建立了一个请求器索引器,用于索引 RequestManger ,后面我们再详细讲。

我们进入最后, 构建 Glide 。

4. 构建Glide,配置数据转换器/解码器/转码器/编码器

回到 Glide 中,看看 Glide 的构造函数,这是一个长得变态的构造函数(有200行),但是不必被它吓倒(好吧,其实第一次看到这里,我是被吓倒了,直接略过去了,限于文章篇幅,这里只截取了部分源码,仔细的话可以直接看源码),仔细分析一下,其实整个构造过程并没那么复杂。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
Glide(
@NonNull Context context,
@NonNull Engine engine,
@NonNull MemoryCache memoryCache,
@NonNull BitmapPool bitmapPool,
@NonNull ArrayPool arrayPool,
@NonNull RequestManagerRetriever requestManagerRetriever,
@NonNull ConnectivityMonitorFactory connectivityMonitorFactory,
int logLevel,
@NonNull RequestOptions defaultRequestOptions,
@NonNull Map<Class<?>, TransitionOptions<?, ?>> defaultTransitionOptions) {

// 步骤1:赋值 GlideBuilder 注入的工具
this.engine = engine;
this.bitmapPool = bitmapPool;
this.arrayPool = arrayPool;
this.memoryCache = memoryCache;
this.requestManagerRetriever = requestManagerRetriever;
this.connectivityMonitorFactory = connectivityMonitorFactory;

DecodeFormat decodeFormat = defaultRequestOptions.getOptions().get(Downsampler.DECODE_FORMAT);
bitmapPreFiller = new BitmapPreFiller(memoryCache, bitmapPool, decodeFormat);

final Resources resources = context.getResources();

// 步骤2:新建注册器
registry = new Registry();
registry.register(new DefaultImageHeaderParser());

// 步骤3:构建编码器和解码器
Downsampler downsampler = new Downsampler(registry.getImageHeaderParsers(),
resources.getDisplayMetrics(), bitmapPool, arrayPool);
ByteBufferGifDecoder byteBufferGifDecoder =
new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), bitmapPool, arrayPool);
ResourceDecoder<ParcelFileDescriptor, Bitmap> parcelFileDescriptorVideoDecoder =
VideoDecoder.parcel(bitmapPool);
ByteBufferBitmapDecoder byteBufferBitmapDecoder = new ByteBufferBitmapDecoder(downsampler);
StreamBitmapDecoder streamBitmapDecoder = new StreamBitmapDecoder(downsampler, arrayPool);
ResourceDrawableDecoder resourceDrawableDecoder =
new ResourceDrawableDecoder(context);
ResourceLoader.StreamFactory resourceLoaderStreamFactory =
new ResourceLoader.StreamFactory(resources);
ResourceLoader.UriFactory resourceLoaderUriFactory =
new ResourceLoader.UriFactory(resources);
ResourceLoader.FileDescriptorFactory resourceLoaderFileDescriptorFactory =
new ResourceLoader.FileDescriptorFactory(resources);
ResourceLoader.AssetFileDescriptorFactory resourceLoaderAssetFileDescriptorFactory =
new ResourceLoader.AssetFileDescriptorFactory(resources);
BitmapEncoder bitmapEncoder = new BitmapEncoder(arrayPool);

BitmapBytesTranscoder bitmapBytesTranscoder = new BitmapBytesTranscoder();
GifDrawableBytesTranscoder gifDrawableBytesTranscoder = new GifDrawableBytesTranscoder();

ContentResolver contentResolver = context.getContentResolver();

// 步骤4:开始注册各个类型对应的解码器和编码器 省略部分代码
registry
.append(ByteBuffer.class, new ByteBufferEncoder())
.append(InputStream.class, new StreamEncoder(arrayPool))
/* Bitmaps */
.append(Registry.BUCKET_BITMAP, ByteBuffer.class, Bitmap.class, byteBufferBitmapDecoder)
.append(Registry.BUCKET_BITMAP, InputStream.class, Bitmap.class, streamBitmapDecoder)
/* BitmapDrawables */
.append(
Registry.BUCKET_BITMAP_DRAWABLE,
ByteBuffer.class,
BitmapDrawable.class,
new BitmapDrawableDecoder<>(resources, byteBufferBitmapDecoder))
/* GIFs */
.append(
Registry.BUCKET_GIF,
InputStream.class,
GifDrawable.class,
new StreamGifDecoder(registry.getImageHeaderParsers(), byteBufferGifDecoder, arrayPool))
.append(Registry.BUCKET_GIF, ByteBuffer.class, GifDrawable.class, byteBufferGifDecoder)
.append(GifDrawable.class, new GifDrawableEncoder())
/* GIF Frames */
// Compilation with Gradle requires the type to be specified for UnitModelLoader here.
.append(
GifDecoder.class, GifDecoder.class, UnitModelLoader.Factory.<GifDecoder>getInstance())
.append(
Registry.BUCKET_BITMAP,
GifDecoder.class,
Bitmap.class,
new GifFrameResourceDecoder(bitmapPool))
/* Drawables */
.append(Uri.class, Drawable.class, resourceDrawableDecoder)
.append(
Uri.class, Bitmap.class, new ResourceBitmapDecoder(resourceDrawableDecoder, bitmapPool))
/* Files */
.register(new ByteBufferRewinder.Factory())
.append(File.class, ByteBuffer.class, new ByteBufferFileLoader.Factory())

// Compilation with Gradle requires the type to be specified for UnitModelLoader here.
.append(File.class, File.class, UnitModelLoader.Factory.<File>getInstance())
/* Models */
.register(new InputStreamRewinder.Factory(arrayPool))
.append(int.class, InputStream.class, resourceLoaderStreamFactory)

/* Transcoders */
.register(
Bitmap.class,
BitmapDrawable.class,
new BitmapDrawableTranscoder(resources))
.register(Bitmap.class, byte[].class, bitmapBytesTranscoder);


// 步骤5:新建图片显示目标对象工厂,并构建 Glide 上下文
ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory();
glideContext =
new GlideContext(
context,
arrayPool,
registry,
imageViewTargetFactory,
defaultRequestOptions,
defaultTransitionOptions,
engine,
logLevel);
}

其中最重要的是步骤3和步骤4,分别为 Glide 初始化了模型转换加载器,解码器,转码器,编码器,并将对各种类型进行一一注册,将其列成表格如下:

  • 模型转换器
转换器 功能
ResourceLoader.StreamFactory 将Android资源ID转换为Uri,在加载成为InputStream
ResourceLoader.UriFactory 将资源ID转换为Uri
ResourceLoader.FileDescriptorFactory 将资源ID转化为ParcelFileDescriptor
ResourceLoader.AssetFileDescriptorFactory 将资源ID转化为AssetFileDescriptor
UnitModelLoader.Factory 不做任何转换,返回源数据
ByteBufferFileLoader.Factory 将File转换为ByteBuffer
FileLoader.StreamFactory 将File转换为InputStream
FileLoader.FileDescriptorFactory 将File转化为ParcelFileDescriptor
DataUrlLoader.StreamFactory 将Url转化为InputStream
  • 解码器
解码器 功能
ByteBufferGifDecoder 将ByteBuffer解码为GifDrawable
ByteBufferBitmapDecoder 将ByteBuffer解码为Bitmap
ResourceDrawableDecoder 将资源Uri解码为Drawable
ResourceBitmapDecoder 将资源ID解码为Bitmap
BitmapDrawableDecoder 将数据解码为BitmapDrawable
StreamBitmapDecoder 将InputStreams解码为Bitmap
StreamGifDecoder 将InputStream数据转换为BtyeBuffer,再解码为GifDrawable
GifFrameResourceDecoder 解码gif帧
FileDecoder 包装File成为FileResource
  • 转码器
转码器 功能
BitmapDrawableTranscoder 将Bitmap转码为BitmapDrawable
BitmapBytesTranscoder 将Bitmap转码为Byte arrays
DrawableBytesTranscoder 将BitmapDrawable转码为Byte arrays
GifDrawableBytesTranscoder 将GifDrawable转码为Byte arrays
  • 编码器
编码器 功能
ByteBufferEncoder 将Byte数据缓存为File
StreamEncoder InputStream缓存为File
BitmapEncoder 将Bitmap数据缓存为File
BitmapDrawableEncoder 将BitmapDrawable数据缓存为File
GifDrawableEncoder 将GifDrawable数据缓存为File
  • 模型转换注册表(实在太多,只列出了部分)
源数据 转换数据 转换器
Integer.class InputStream.class ResourceLoader.StreamFactory
String.class InputStream.class DataUrlLoader.StreamFactory
Uri.class InputStream.class DataUrlLoader.StreamFactory

以上模型转换注册表非常重要,在Glide进入解码流程时,将会遍历这里注册的所有可能转换的情形,尝试进行数据转换。

这里只列出部分情形,其它还包括 File/Bitmap/Drawable/Byte 等等几乎涵括了日常使用的情况。

Glide的加载流程可以概括为以下流程:

model(数据源)-->data(转换数据)-->decode(解码)-->transformed(缩放)-->transcoded(转码)-->encoded(编码保存到本地)

其中,transformed 为对解码得到的图片数据进行缩放,如 FitCenter、CropCenter 等。

到这里,Glide 单例就构建完成了,让我们返回到 Glide#with 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

@NonNull
public static RequestManager with(@NonNull FragmentActivity activity) {
return getRetriever(activity).get(activity);
}

@NonNull
private static RequestManagerRetriever getRetriever(@Nullable Context context) {
// Context could be null for other reasons (ie the user passes in null), but in practice it will
// only occur due to errors with the Fragment lifecycle.
Preconditions.checkNotNull(
context,
"You cannot start a load on a not yet attached View or a Fragment where getActivity() "
+ "returns null (which usually occurs when getActivity() is called before the Fragment "
+ "is attached or after the Fragment is destroyed).");
return Glide.get(context).getRequestManagerRetriever();
}

在构建好 Glide 后,通过 getRequestManagerRetriever() 将会得到一个 RequestManagerRetriever ,即 RequestManager 的检索器,RequestManagerRetriever#get() 将为每个请求页面创建一个 RequestManager。

还记得 GlideBuilder#build 提到的一句代码吗?

1
2
RequestManagerRetriever requestManagerRetriever =
new RequestManagerRetriever(requestManagerFactory);

没错,这里获取的就是它。这里就必须要讲到 Glide 数据请求的生命周期了。

我们都知道 Glide 会根据页面的生命周期来自动的开启和结束数据的请求,那么 Glide 是怎么做到的呢?

5. 生命周期管理

我们进入 RequestManagerRetriever#get(Activity) 方法中。

1
2
3
4
5
6
7
8
9
10
11
12
@SuppressWarnings("deprecation")
@NonNull
public RequestManager get(@NonNull Activity activity) {
if (Util.isOnBackgroundThread()) {
return get(activity.getApplicationContext());
} else {
assertNotDestroyed(activity);
android.app.FragmentManager fm = activity.getFragmentManager();
return fragmentGet(
activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
}
}

首先,判断是否为后台线程,如果是,则使用 ApplicationContext 重新获取。
重点来看 else 代码块。先断言请求的页面是否已经销毁。否则获取当前页面的 FragmentManager,并传给 fragmentGet 方法。

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
@SuppressWarnings({"deprecation", "DeprecatedIsStillUsed"})
@Deprecated
@NonNull
private RequestManager fragmentGet(@NonNull Context context,
@NonNull android.app.FragmentManager fm,
@Nullable android.app.Fragment parentHint,
boolean isParentVisible) {
RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
RequestManager requestManager = current.getRequestManager();
if (requestManager == null) {
// TODO(b/27524013): Factor out this Glide.get() call.
Glide glide = Glide.get(context);
requestManager =
factory.build(
glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
current.setRequestManager(requestManager);
}
return requestManager;
}

@NonNull
SupportRequestManagerFragment getSupportRequestManagerFragment(FragmentActivity activity) {
return getSupportRequestManagerFragment(
activity.getSupportFragmentManager(), /*parentHint=*/ null, isActivityVisible(activity));
}

在 fragmentGet 中首先通过 getRequestManagerFragment() 来获取一个命名为 FRAGMENT_TAG 的 fragment,如不存在,则新建一个 RequestManagerFragment,并添加到当前页面中。

这里我们就可以猜到了,Glide 是通过在页面中添加一个 Fragment 来动态监听页面的创建和销毁,从而达到依赖页面生命周期,动态管理请求的目的。

在 RequestManagerFragment 构造函数中,注入了一个生命周期监听器 ActivityFragmentLifecycle,并在 Fragment 各个生命周期回调中,调用了对应的方法。

而 ActivityFragmentLifecycle 也紧接着会调用 lifecycleListener 监听器,而这个监听器其实就是 RequestManger。如下:

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
class ActivityFragmentLifecycle implements Lifecycle {
private final Set<LifecycleListener> lifecycleListeners =
Collections.newSetFromMap(new WeakHashMap<LifecycleListener, Boolean>());
private boolean isStarted;
private boolean isDestroyed;

@Override
public void addListener(@NonNull LifecycleListener listener) {
lifecycleListeners.add(listener);

if (isDestroyed) {
listener.onDestroy();
} else if (isStarted) {
listener.onStart();
} else {
listener.onStop();
}
}

@Override
public void removeListener(@NonNull LifecycleListener listener) {
lifecycleListeners.remove(listener);
}

void onStart() {
isStarted = true;
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onStart();
}
}

void onStop() {
isStarted = false;
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onStop();
}
}

void onDestroy() {
isDestroyed = true;
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onDestroy();
}
}
}

最后,RequestManagerRetriever#fragmentGet,判断这个Fragment的RequestManager是否存在,否则创建一个RequestManager,并将生命周期注入,同时RquestManager构建时,将会通过addListener注入生命周期回调(具体可以查看RequestManger构造函数)。

最后,Glide#with终将得到一个RequestManager。

至此,Glide的加载过程就解析完毕了。总结一下整个流程:

  • 通过AndroidManifest和@GlideModule注解获取用户自定义配置GlideModule,并调用其对应的方法
  • 通过GlideBuilder构建Glide:
    1. 新建线程池
    2. 新建图片缓存池和缓存池
    3. 新建内存缓存管理器
    4. 新建默认本地缓存管理器
    5. 新建请求引擎Engine
    6. 新建RequestManger检索器
    7. 新建Glide
  • Glide构造方法中,新建模型转换器,解码器,转码器,编码器,以及生成Glide上下文GlideContext
  • 通过RequestManager检索器,建立生命周期监听,并建立一个RequestManager

Glide与GlideApp

如果在项目中已经使用了 Glide3.x ,并且想要升级到 Glide4.x ,那么你会发现,原来使用链式调用进行参数配置的方法已经被修改了,同一个封装到了 RequesOptions 中,如下:

1
2
3
4
5
6
7
8
9
10
RequestOptions options = new RequestOptions()
.centerCrop()
.placeholder(R.mipmap.ic_launcher_round)
.error(R.mipmap.ic_launcher)
.priority(Priority.HIGH)
.diskCacheStrategy(DiskCacheStrategy.NONE);
Glide.with(this)
.load(ImageConfig.URL_GIF)
.apply(options)
.into(iv);

这样的话升级后将导致大量的修改,当然你也可以自己封装一下,但是 Glide 已经为我们做好了兼容方案。

还记得初始化是通过 @GlideModule 注解来注册自定义配置吗?只要在项目中定义这么一个配置,那么 Glide 将会自动帮我们生成一个 GlideApp 模块,封装了 Glide3.x 中的调用方式。

1
2
3
4
5
6
public class GlideConfiguration extends AppGlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {

}
}

调用如下,还是原来的配方,还是熟悉的味道~

1
2
3
4
5
6
7
GlideApp.with(this)
.load(ImageConfig.URL_WEBP)
.sizeMultiplier(0.5f)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.error(R.mipmap.ic_launcher)
.into(iv);

如果你还觉得不爽,那么你甚至可以把GlideApp直接修改为Glide,实现几乎“无缝对接”。当然,你还是要修改引用路径的。

1
2
3
4
5
6
7
@GlideModule(glideName="Glide")
public class GlideConfiguration extends AppGlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {

}
}

来源:

收藏

Java初始化顺序

无继承情况下的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
class Sample{
Sample(String s){
System.out.println(s);
}
Sample(){
System.out.println("Sample默认构造函数被调用");
}
}
class Test{

static Sample sam=new Sample("静态成员sam初始化");
Sample sam1=new Sample("sam1成员初始化");

static{
System.out.println("static块执行");
if(sam==null)System.out.println("sam is null");
sam=new Sample("静态块内初始化sam成员变量");
}

Test(){
System.out.println("Test默认构造函数被调用");
}
}
//主函数
public static void main(String str[]){
Test a=new Test();
}

输出结果为:

1
2
3
4
5
静态成员sam初始化     -----静态成员初始化
static块执行 -----静态块被执行
静态块内初始化sam成员变量 ----静态块执行
sam1成员初始化 -----普通成员初始化
Test默认构造函数被调用 -----构造函数执行

由此可以得出结论:

  • 静态成员变量首先初始化(注意,Static可以看做一个静态成员,其执行顺序和其在类中申明的顺序有关)
  • 普通成员初始化
  • 执行构造函数。

继承情况下的初始化顺序

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
class Test {
static {
System.out.println("父类static 块 1 执行");
}
static Sample staticSam1 = new Sample("父类 静态成员staticSam1初始化");
Sample sam1 = new Sample("父类 sam1成员初始化");
static Sample staticSam2 = new Sample("父类 静态成员staticSam2初始化");
static {
System.out.println("父类 static 块 2 执行");
}

Test() {
System.out.println("父类 Test默认构造函数被调用");
}

Sample sam2 = new Sample("父类 sam2成员初始化");

}

class TestSub extends Test {
static Sample staticSamSub = new Sample("子类 静态成员staticSamSub初始化");

TestSub() {
System.out.println("子类 TestSub 默认构造函数被调用");
}

Sample sam1 = new Sample("子类 sam1成员初始化");
static Sample staticSamSub1 = new Sample("子类 静态成员staticSamSub1初始化");

static {
System.out.println("子类 static 块 执行");}

Sample sam2 = new Sample("子类 sam2成员初始化");
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
父类 static 块 1  执行
父类 静态成员staticSam1初始化
父类 静态成员staticSam2初始化
父类 static 块 2 执行
--------父类静态成员初始化
子类 静态成员staticSamSub初始化
子类 静态成员staticSamSub1初始化
子类 static 块 执行
-------子类静态成员初始化
父类 sam1成员初始化
父类 sam2成员初始化
父类 Test默认构造函数被调用
-------父类普通成员初始化和构造函数执行
子类 sam1成员初始化
子类 sam2成员初始化
子类 TestSub 默认构造函数被调用
-------父类普通成员初始化和构造函数执行

由此得出Java初始化顺序结论:

  • 继承体系的所有静态成员初始化(先父类,后子类)
  • 父类初始化完成(普通成员的初始化–>构造函数的调用)
  • 子类初始化(普通成员–>构造函数)

Java初始化顺序如图:
image

函数式编程

函数式编程

编程范式:

函数式编程是一种编程范式,我们常见的编程范式有,命令式编程(Imperative programming),函数式编程, 逻辑式编程,常见的面向对象编程是也是一种命令式编程。

本质

函数式编程中的“函数”这个术语不是指计算机中的函数(实际上是“Subroutine”),而是指数学中的函数,即自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。

比如sqrt(x)函数计算x的平方根,只要x不变,不论什么时候调用,调用几次,值都是不变的。

函数式编程关心数据的映射,命令式编程关心解决问题的步骤(更数学化一点的描述:函数式编程关心类型(代数结构)之间的关系,命令式编程关心解决问题的步骤)

在函数式语言中,函数作为一等公民,可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,可以对函数进行组合。

纯函数式编程语言中的变量也不是命令式编程语言中的变量,即存储状态的单元,而是代数中的变量,即一个值的名称。变量的值是不可变的(immutable),也就是说不允许像命令式编程语言中那样多次给一个变量赋值。比如说在命令式编程语言我们写“x = x + 1”,这依赖可变状态的事实,拿给程序员看说是对的,但拿给数学家看,却被认为这个等式为假。

严格意义上的函数式编程意味着不使用可变的变量,赋值,循环和其他命令式控制结构进行编程。

特点

  • 与面向对象编程(Object-oriented programming)和过程式编程(Procedural programming)并列的编程范式。
  • 最主要的特征是,函数是第一等公民。
  • 强调将计算过程分解成可复用的函数,典型例子就是map方法和reduce方法组合而成 MapReduce 算法。
  • 只有纯的、没有副作用的函数,才是合格的函数。
  • 不变性。

好处

由于命令式编程语言也可以通过类似函数指针的方式来实现高阶函数,函数式的最主要的好处主要是不可变性带来的。没有可变的状态,函数就是引用透明(Referential transparency)的和没有副作用(No Side Effect)。

一个好处是,函数即不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,这样写的代码容易进行推理,不容易出错。这使得单元测试和调试都更容易。

不变性带来的另一个好处是:由于(多个线程之间)不共享状态,不会造成资源争用(Race condition),也就不需要用锁来保护可变状态,也就不会出现死锁,这样可以更好地并发起来,尤其是在对称多处理器(SMP)架构下能够更好地利用多个处理器(核)提供的并行处理能力。

参考:

  1. 函数式编程入门教程
  2. 什么是函数式编程思维?nameoverflow
  3. 什么是函数式编程思维?用心阁

AIDL

Book.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.honszeal.processdemo;

import android.os.Parcel;
import android.os.Parcelable;

/**
* date:2018/3/13 on 16:07
* desc:
* author: liuch
*/
public class Book implements Parcelable{

private int bookId;
private String bookName;

protected Book(Parcel in) {
bookId = in.readInt();
bookName = in.readString();
}

public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}

@Override
public Book[] newArray(int size) {
return new Book[size];
}
};

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bookId);
dest.writeString(bookName);
}
}

IDE 自动完成

Book.aidl

1
2
3
package com.honszeal.processdemo;

parcelable Book;

ILibraryManager.aidl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.honszeal.processdemo;

import com.honszeal.processdemo.Book;


interface ILibraryManager {

List<Book> getNewBookList();

void donateBook(in Book book);

int add(int a, int b);

void log(String message);

}

编译一下

切换到 project 选项卡,在 app -> build -> generated -> source -> aidl -> debug -> 包名 下 找到系统生成的ILibraryManager.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package com.honszeal.processdemo;

public interface ILibraryManager extends android.os.IInterface {

public static abstract class Stub extends android.os.Binder implements com.honszeal.processdemo.ILibraryManager {

private static final java.lang.String DESCRIPTOR = "com.honszeal.processdemo.ILibraryManager";

public Stub() {
this.attachInterface(this, DESCRIPTOR);
}

public static com.honszeal.processdemo.ILibraryManager asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.honszeal.processdemo.ILibraryManager))) {
return ((com.honszeal.processdemo.ILibraryManager) iin);
}
return new com.honszeal.processdemo.ILibraryManager.Stub.Proxy(obj);
}

@Override
public android.os.IBinder asBinder() {
return this;
}

@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getNewBookList: {
data.enforceInterface(DESCRIPTOR);
java.util.List<com.honszeal.processdemo.Book> _result = this.getNewBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_donateBook: {
data.enforceInterface(DESCRIPTOR);
com.honszeal.processdemo.Book _arg0;
if ((0 != data.readInt())) {
_arg0 = com.honszeal.processdemo.Book.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
this.donateBook(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}

private static class Proxy implements com.honszeal.processdemo.ILibraryManager {
private android.os.IBinder mRemote;

Proxy(android.os.IBinder remote) {
mRemote = remote;
}

@Override
public android.os.IBinder asBinder() {
return mRemote;
}

public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}

@Override
public java.util.List<com.honszeal.processdemo.Book> getNewBookList() throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.honszeal.processdemo.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getNewBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.honszeal.processdemo.Book.CREATOR);
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}

@Override
public void donateBook(com.honszeal.processdemo.Book book) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book != null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_donateBook, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}

static final int TRANSACTION_getNewBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_donateBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}

public java.util.List<com.honszeal.processdemo.Book> getNewBookList() throws android.os.RemoteException;

public void donateBook(com.honszeal.processdemo.Book book) throws android.os.RemoteException;

}

BookService.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
public class BookService extends Service {

private Binder binder = new ILibraryManager.Stub() {
@Override
public List<Book> getNewBookList() throws RemoteException {
return null;
}

@Override
public void donateBook(Book book) throws RemoteException {

}
};

@Override
public void onCreate() {
super.onCreate();
}

@Override
public IBinder onBind(Intent intent) {
return binder;
}
}

手写

IBookManager.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.honszeal.processdemo;

import android.os.IInterface;
import android.os.RemoteException;

import java.util.List;

public interface IBookManager extends IInterface {

String DESCRIPTOR = "com.honszeal.processdemo.ILibraryManager";
int TRANSACTION_getNewBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
int TRANSACTION_donateBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);

List<Book> getNewBookList() throws RemoteException;

void donateBook(Book book) throws RemoteException;
}

BookManagerImpl

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package com.honszeal.processdemo;

import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;

import java.util.List;

public class BookManagerImpl extends Binder implements IBookManager {

public BookManagerImpl() {
this.attachInterface(this, DESCRIPTOR);
}

public static IBookManager asInterface(IBinder obj) {
if (obj == null) {
return null;
}

IInterface iin = obj.queryLocalInterface(DESCRIPTOR);

if (iin != null && iin instanceof IBookManager) {
return ((IBookManager) iin);
}
return new BookManagerImpl.Proxy(obj);
}

@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
switch (code) {
case INTERFACE_TRANSACTION:
reply.writeString(DESCRIPTOR);
return true;
case TRANSACTION_getNewBookList:
data.enforceInterface(DESCRIPTOR);
List<Book> bookList = this.getNewBookList();
reply.writeNoException();
reply.writeTypedList(bookList);
return true;
case TRANSACTION_donateBook:
data.enforceInterface(DESCRIPTOR);
Book arg0;
if (0 != data.readInt()) {
arg0 = Book.CREATOR.createFromParcel(data);
} else {
arg0 = null;
}
this.donateBook(arg0);
reply.writeNoException();
return true;
}
return super.onTransact(code, data, reply, flags);
}

@Override
public List<Book> getNewBookList() throws RemoteException {
return null;
}

@Override
public void donateBook(Book book) throws RemoteException {

}

@Override
public IBinder asBinder() {
return this;
}

private static class Proxy implements IBookManager {

private IBinder mRemote;

public Proxy(IBinder mRemote) {
this.mRemote = mRemote;
}

public String getInterfaceDescriptor() {
return DESCRIPTOR;
}

@Override
public List<Book> getNewBookList() throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();

List<Book> result;

data.writeInterfaceToken(getInterfaceDescriptor());
mRemote.transact(TRANSACTION_getNewBookList, data, reply, 0);
reply.readException();
result = reply.createTypedArrayList(Book.CREATOR);
return result;
}

@Override
public void donateBook(Book book) throws RemoteException {
//
}

@Override
public IBinder asBinder() {
return mRemote;
}
}
}

感觉,就是具体的实现在服务端写,在客户端调用。实际上就是调用了 Proxy 的对应方法,然后 Bindertransact 方法 再就是 onTransact 方法。

权限验证

  1. onBind 中进行验证,验证不通过就直接返回 null, 这样验证失败的客户端直接无法绑定服务。
  2. onTransact 中进行验证,验证不通过就直接返回 false
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×

keyboard_arrow_up 回到顶端