潘万坤

  • 首页
  • 关于
  • 归档

Android 主要的热修复方案原理分析

发表于 2016-09-29

目前较为成熟的热修复框架主要有AndFix、Nuwa以及微信的热更新思想。现在将其主要思想总结如下:

AndFix

AndFix是支付宝开源的一套热修复框架,使用简单,成功率高,基本满足大多数的bug修复场景。引入到项目中非常方便,主要分两步:

  • 代码整合

    • build.gradle添加依赖 compile ‘com.alipay.euler:andfix:0.4.0@aar’
    • Application.onCreate()方法中添加

      1
      2
      3
      PatchManager patchManager = new PatchManager(this);
      patchManager.init(appversion);//current version
      patchManager.loadPatch();

      然后和后端协商一个补丁包下载服务器,在每次下载更新包到本地后

      1
      patchManager.addPatch(path);
  • 打补丁
    AndFix提供了一个打补丁包的工具,可以去这里下载,使用方法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
    -a,--alias <alias> keystore entry alias.
    -e,--epassword <***> keystore entry password.
    -f,--from <loc> new Apk file path.
    -k,--keystore <loc> keystore path.
    -n,--name <name> patch name.
    -o,--out <dir> output dir.
    -p,--kpassword <***> keystore password.
    -t,--to <loc> old Apk file path.

AndFix的思想是直接更改修复的方法,具体我们可以看源码。先从PatchManager的init和load方法入手,这两个方法实现了补丁包的加载并最终调用了AndFixManager的fix方法:

1
2
3
4
5
6
7
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
if (patchNames.contains(patchName)) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), classLoader, classes);
}
}

fix函数需要传入三个参数:patch文件、classloader以及需要fix的class列表。fix函数代码如下:

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
/**
* fix
*
* @param file
* patch file
* @param classLoader
* classloader of class that will be fixed
* @param classes
* classes will be fixed
*/
public synchronized void fix(File file, ClassLoader classLoader,
List<String> classes) {
.......省略........
//load dex文件
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);
if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}
// 定义自己的classloader
ClassLoader patchClassLoader = new ClassLoader(classLoader) {
@Override
protected Class<?> findClass(String className)
throws ClassNotFoundException {
Class<?> clazz = dexFile.loadClass(className, this);
if (clazz == null
&& className.startsWith("com.alipay.euler.andfix")) {
return Class.forName(className);// annotation’s class
// not found
}
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
Enumeration<String> entrys = dexFile.entries();
Class<?> clazz = null;
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
clazz = dexFile.loadClass(entry, patchClassLoader);
if (clazz != null) {
fixClass(clazz, classLoader);
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}

这个方法的作用是load dex文件中的类,并依次修复,这个函数中有两处疑问:

  • classes参数设计有何深意,因为我理解的patch包里面难道不都是需要修复的类吗,还会把没有修改的类打进去吗?
  • 自定义了一个classloader,针对”com.alipay.euler.andfix”做了特殊处理,不知道怎么才会有这种场景。
    针对这两个问题我特意咨询了AndFix的作者黎三平大神,大神给我的答复是:1. 这个设计有两个原因:
    a) 新增类
    b)早期patch工具打出的补丁包不是很准确
    2.AndFix的一个注解,它的类加载会走到这来的。
    大神的话还是不是很明白,大家如果看到了这块代码请帮我解释一下。

fix函数中遍历dex的类,并过滤掉不需要修复的类后调用fixclass函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
if (!isEmpty(clz) && !isEmpty(meth)) {
replaceMethod(classLoader, clz, meth, method);
}
}
}

代码很简洁,意思也很明了,就是找到这个类中需要修复的函数然后调用replaceMethod方法。替换方法在java层是无法做到的,所以这个函数最终还是调用了native的替换函数的方法,实质就是更改了类中方法所指向的地址,所以java不能做到。

jin的目录结构如下:
AndFix Jni结构.png

AndFix做了dalvik、art以及各平台的适配,核心是方法的替换,我们来看其中一个方法替换的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
clz->status = CLASS_INITIALIZED;
Method* meth = (Method*) env->FromReflectedMethod(src);
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
meth->clazz = target->clazz;
meth->accessFlags |= ACC_PUBLIC;
meth->methodIndex = target->methodIndex;
meth->jniArgInfo = target->jniArgInfo;
meth->registersSize = target->registersSize;
meth->outsSize = target->outsSize;
meth->insSize = target->insSize;
meth->prototype = target->prototype;
meth->insns = target->insns;
meth->nativeFunc = target->nativeFunc;
}

含义估计大家都明白,就是把方法的各个属性值替换,实际去写确实还是很有难度。

至此我把AndFix代码的主要流程梳理了一遍,其中还有很多没有get到的点,思想基本清楚了,AndFix主要采用替换方法的方式进行热修复,好处是立即生效且补丁包较小,但是只能基于方法修复,而且对平台的兼容性不佳,但不失为一个伟大的想法,也是热修复最早开源的修复方案,向我的黎三平大神说声感谢!

Nuwa

AndFix的思路很简单,直接在native层替代方法,有没有更简单的呢,有,Nuwa!他的想法就更自然一些,直接替换类,或者废弃掉有bug的类,怎么做到的呢,核心就在于java.lang.ClassLoader.java这个类的loadClass方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}

同名的类加载一次就不再加载了,是不是想到什么了,哈哈,对,就是把你要修复的类提前加载就ok了,那么有bug的类便不再加载进来,实现起来也非常简单,三行代码搞定,我们看Nuwa的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
//新建补丁包的dexclassloader
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
//获取原dex加载列表
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
// 新建补丁包dex加载列表
Object newDexElements = getDexElements(getPathList(dexClassLoader));
//补丁包插在最前连接成新的dex加载列表
Object allDexElements = combineArray(newDexElements, baseDexElements);
Object pathList = getPathList(getPathClassLoader());
//利用发射修改dalvik.system.BaseDexClassLoader类的pathList中的dexElements属性
ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
}

就这么几行代码就实现了替换类的热修复,没有native的操作,思路清晰,含义明确。当然这里面还有一个坑就是类加载的时候会有一个标记一个类和另外在同一个dex中的标记,所以打出补丁包之后会报 “两个类所在的dex不在一起” 的错误,这个也好办,打出一个单独的dex,里面只有一个类,让每个类都引用这个类,这样就使得每个类的标记都是false。Nuwa实现了这个插入引用单独类的插件,对使用者非常友好。

Nuwa能自由的修改和添加类,功能更加强大,不过这里面隐含了一个缺陷就是hook classloader的dexElements必须在所有类没有加载之前进行,所以一般放在application的oncreate方法中,这样就导致了每次发布补丁必须重启app才能生效。

微信的热修复方案

其实技术方案的迭代也是思绪不断延伸的过程,看过Nuwa的热修复方案之后,是不是会想到有没有更简单更优雅的方式,有!其实很容易想到,我们在客户端实现补丁包的逻辑是什么呢,无非是比较两个dex中类文件发生更改的类提取出来,打成新的补丁dex,那这个过程能不能在客户端逆向操作一次呢,直接将差分包和原dex进行融合,形成新的dex,这样代码就不用做任何修改了,答案是肯定的。微信在一篇博客中阐述了自己热修复方案的主要思路(微信Android热补丁实践演进之路)。没有具体实现,主要可能是文件权限的一些坑,但是像微信这样的app架构中,肯定是做了精准的分包处理,自己管理dex的加载策略,所以实现起来应该非常顺利。
微信在文中也坦言做热修复起源于15年6月,相对较晚,也可以综合比较之后设计出适合自己的方案。

以上是目前比较成熟的几个热修复方案,只是整理了主要思想,还有很多黑科技没有get到,希望对大家能有所帮助。

RecyclerView高端定制三部曲

发表于 2016-09-29

RecyclerView随V7拓展包发布以来,因其高效和使用便利,基本取代了listview和gridview,成为了使用频率最高的控件之一。默认的设置基本能满足大部分场景,如果需要更好的体验,需要自定义以下三个部分的内容:

  • Animator
  • ItemDecoration
  • LayoutManager

自定义Animator

自定义Animator可以实现各种绚丽的动画效果,RecyclerView动画相关的类主要有三个:

  • RecyclerView.ItemAnimator
  • SimpleItemAnimator
  • DefaultItemAnimator

RecyclerView.ItemAnimator是自定义RecyclerView动画效果的核心类,当继承一个ItemAnimator时,有如下几个方法需要被实现:

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
//当一个ViewHolder从RecyclerView里面消失时调用,不一定是移除,也有可能是move操作
@Override  
    public boolean animateDisappearance(RecyclerView.ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {  
        return false;  
    }  
  //当一个ViewHolder从RecyclerView里面显示时调用,不一定是新增,也有可能是move操作
    @Override  
    public boolean animateAppearance(RecyclerView.ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {  
        return false;  
    }  
  //没有调用notify而引起布局的改变,比如滑动
    @Override  
    public boolean animatePersistence(RecyclerView.ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {  
        return false;  
    }  
  //item发生改变的时候调用
    @Override  
    public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {  
        return false;  
    }  
  //统筹RecyclerView中所有的动画,统一启动执行,一般的思路是在前面几个函数调用中放入一个动画列表,在这个函数中统一执行
    @Override  
    public void runPendingAnimations() {  
  
    }  
  //结束某一个item的动画
    @Override  
    public void endAnimation(RecyclerView.ViewHolder item) {  
    }  
  //结束所有的动画
    @Override  
    public void endAnimations() { 
    }  
  //动画是否执行过程中
    @Override  
    public boolean isRunning() {  
        return false;  
}

SimpleItemAnimator对RecyclerView.ItemAnimator实现了简单的封装,将基本的ItemAnimator不同场景拆分成我们熟悉的四种场景:add、remove、move、change。所以我们如果实现自定义的动画,继承自SimpleItemAnimator会更容易实现,这也是较为普遍的做法。

DefaultItemAnimator是RecyclerView默认的动画效果,只有一个fadein和fadeout的渐变动画,开始看代码的时候一直有一个疑问,默认的动画效果明明是一个先展开然后插入的动画啊,哪里是渐变的效果。对比了各种动画效果之后发现,这里实现的动画效果只是针对item出现或者消失时的动画,位置展开是RecylerView固定的效果,具体代码没有去源码中跟踪,调用notifyItemInserted函数刷新界面时,先执行对应位置的展开再执行item的动画效果,我们自定义动画就是实现这个item出现的方式,位置展开和收缩是固定的。V7拓展包23.0版本DefaultItemAnimator继承自RecyclerView.ItemAnimator,23.1版本就直接继承自SimpleItemAnimator。

我们参照DefaultItemAnimator的方式实现我们自定义的动画效果。其中最主要的删除和新增实现如下:

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
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mRemoveAnimations.add(holder);
animation.setDuration(getRemoveDuration())
.alpha(0).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
ViewCompat.setAlpha(view, 1);
dispatchRemoveFinished(holder);
mRemoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
private void animateAddImpl(final RecyclerView.ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mAddAnimations.add(holder);
animation.alpha(1).setDuration(getAddDuration()).
setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
ViewCompat.setAlpha(view, 1);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchAddFinished(holder);
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}

其中写死了animation的效果,而又无法重载,我们将DefaultItemAnimator的代码完全拷出,实现一个可配置动画的BaseItemAnimator类。主要替换的代码如下:

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
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
mRemoveAnimations.add(holder);
final ViewPropertyAnimatorCompat animation = getRemoveAnimator(holder);
animation.setDuration(getRemoveDuration()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
clear(view);
dispatchRemoveFinished(holder);
mRemoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
});
animation.start();
}
private void animateAddImpl(final RecyclerView.ViewHolder holder) {
mAddAnimations.add(holder);
final ViewPropertyAnimatorCompat animation = getAddAnimator(holder);
animation.setDuration(getAddDuration()).
setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
ViewCompat.setAlpha(view,1);
dispatchAddStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
clear(view);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchAddFinished(holder);
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
protected abstract ViewPropertyAnimatorCompat getAddAnimator(RecyclerView.ViewHolder viewHolder);
protected abstract ViewPropertyAnimatorCompat getRemoveAnimator(RecyclerView.ViewHolder viewHolder);
public void clear(View v) {
ViewCompat.setAlpha(v, 1);
ViewCompat.setScaleY(v, 1);
ViewCompat.setScaleX(v, 1);
ViewCompat.setTranslationY(v, 0);
ViewCompat.setTranslationX(v, 0);
ViewCompat.setRotation(v, 0);
ViewCompat.setRotationY(v, 0);
ViewCompat.setRotationX(v, 0);
ViewCompat.setPivotY(v, v.getMeasuredHeight() / 2);
ViewCompat.setPivotX(v, v.getMeasuredWidth() / 2);
ViewCompat.animate(v).setInterpolator(null).setStartDelay(0);
}

接下来我们就可以继承BaseItemAnimator类实现自己的动画效果,下面给出一个示例:

1
2
3
4
5
6
7
8
9
@Override
protected ViewPropertyAnimatorCompat getAddAnimator(RecyclerView.ViewHolder item) {
ViewCompat.setTranslationX(item.itemView, -item.itemView.getWidth());
return ViewCompat.animate(item.itemView).translationX(0);
}
@Override
protected ViewPropertyAnimatorCompat getRemoveAnimator(RecyclerView.ViewHolder item) {
return ViewCompat.animate(item.itemView).translationX(item.itemView.getWidth());
}

详细代码见github

自定义ItemDecoration

自定义ItemDecoration需要继承RecyclerView.ItemDecoration抽象类,源码很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static abstract class ItemDecoration {
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
@Deprecated
public void onDraw(Canvas c, RecyclerView parent) {
}
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
@Deprecated
public void onDrawOver(Canvas c, RecyclerView parent) {
}
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}

官方推荐使用含有state参数的方法,所以主要就是重载三个方法:

  • onDraw(Canvas c, RecyclerView parent, State state)
  • onDrawOver(Canvas c, RecyclerView parent, State state)
  • getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)

onDraw用于绘制divider,它是绘制在item下面的,所以中间部分会被item遮挡住;
onDrawOver绘制在item上面,所以不受位置的限制;
getItemOffsets实际上就是给每个item一个padding,实现item之间的间隙。

实际业务中我们经常会遇到这样的需求,一个可选择的gridview,选中与否有不同的边框,效果图如下:

设计图

以我有限的界面开发经验,不管如何控制,要实现每个分割线都是相同的宽度,选中界面的边框正好压盖周围的边框还是很有难度的,看到recyclerview的自定义ItemDecoration才终于发现一道曙光。

我的做法是在getItemOffsets函数中,我判断一个item上下左右是否有临近的item,没有的话给两倍的dividerwidth,否则一倍的dividerwidth,这样就能控制所有的item四周都是有同样宽度的间隙。然后在onDrawOver中实现每个item分割线的绘制,同时判断是否选中,再绘制选中的边框,整体效果如下:

效果图

还是完美的实现了UI妹子要求的效果(^_^)。

详细代码见github

自定义LayoutManager

LayoutManager可以说是整个RecyclerView的精髓,整个RecyclerView的Recycler也是在LayoutManager做的,官方目前提供了LinearLayoutManager、GridLayoutManager和StaggeredGridLayoutManager三种LayoutManager,分别使用在线性、方格以及不规则瀑布流的场景,基本上实现了日常的大部分需求。目前精力有限,后续再做分解。

Dagger2从入门到会用再到暂时放弃

发表于 2016-09-29

Dagger2是一种依赖注入框架,目前由Google维护。说到依赖注入,标准定义是目标类中所依赖的其他的类的初始化过程,不是通过手动编码的方式创建,而是通过技术手段可以把其他的类的已经初始化好的实例自动注入到目标类中。说简单就是一次构建,到处注入。目前只能理解这个定义,具体的应用场景坑还没踩够,还不能意会。

这两天也看了不少Dagger2的Blog,大部分都翻译自tasting-dagger-2-on-android 这篇文章再修修改改,参考的工程主要是Android-CleanArchitecture 关于Clean框架用Dagger实现以及官方咖啡机的故事。主要实现的代码总结起来是以下几行代码:

1
2
3
4
5
6
7
public class Hello {
@Inject Hello() { //Inject注解构造函数
}
public void say() {
System.out.println(“hello world”);
}
}

1
2
3
4
@Component //Component组件绑定宿主
public interface HelloComponent {
void inject(HelloDagger dagger);
}
1
2
3
4
5
6
7
8
9
public class HelloDagger {
@Inject
static Hello hello;//声明注入依赖
public static void main(String[] args) {
DaggerHelloComponent.create().inject(new HelloDagger());//Dagger默认生成DaggerHelloComponent类,调用inject方法注入对象
hello.say(); //对象已经初始化了
}
}

这是最简单的一个Dagger2的实例,当然Hello类是无参的构造函数,如果有参构造的话还需要引入module和provides两个注解,代码如下:

1
2
3
4
5
6
7
8
9
public class Hello {
private String hello;
@Inject Hello(String hello) { //有参构造函数
this.hello = hello;
}
public void say() {
System.out.println(hello);
}
}

1
2
3
4
5
6
7
8
9
10
@Module
public class HelloModule {
String hello = "hello";
public HelloModule(String hello) {
this.hello = hello;
}
@Provides Hello getHello(){ //Provides注解返回依赖的对象
return new Hello(hello);
}
}
1
2
3
4
@Component(modules = HelloModule.class) //绑定依赖对象和宿主
public interface HelloComponent {
void inject(HelloDagger dagger);
}
1
2
3
4
5
6
7
8
9
10
11
public class HelloDagger {
@Inject
static Hello hello;
public static void main(String[] args) {
DaggerHelloComponent.builder()
.helloModule(new HelloModule("hello world")) //创建module对象
.build()
.inject(new HelloDagger());//注入
hello.say();
}
}

比较常用的就是@Inject、@Module、@Provides以及@Component注解,inject在宿主中声明依赖对象,module和provides配合初始化依赖对象,component连接彼此,完成注入过程。整个过程就是一个A a = new A()的过程,只是彼此分离,还是一个解耦的目的。

在构建代码的过程中我们可能还感受不到依赖注入的好处,当程序在膨胀,业务不断累积的过程中,我们发现我们很难遵守面向对象六大原则之依赖倒置原则。如果我们使用了Dagger,当依赖对象发生改变时,我们的Inject和component模块都不需要改变,只需要改变module模块,而这一模块相对独立,变动起来真的随心所欲。

Dagger2终归还是一个模块解耦的利器,坑还没踩够,所以体会不到真正的好处,只能暂时放弃了。就像Fernando Cejas说的,Dagger2的好处主要有三点:

  • Since dependencies can be injected and configured externally we can reuse those components.
  • When injecting abstractions as collaborators, we can just change the implementation of any object without having to make a lot of changes in our codebase, since that object instantiation resides in one place isolated and decoupled.
  • Dependencies can be injected into a component: it is possible to inject mock implementations of these dependencies which makes testing easier.

最后大神镇楼
Fernando Cejas.png

Fresco源码浅析-ImagePipeline模块(三)

发表于 2016-09-29

ImagePipeline是Fresco读取数据的整个调度系统,作为一个图片加载组件,主要工作流程为:

  • 检查内存缓存
  • 检查磁盘缓存
  • 文件读取或网络请求,并存储到各个缓存。
    官方流程图如下:
    imagepipeline.png

这和主要的图片加载逻辑基本类似,既然如此,那我们就从图片加载组件最主要的两个方面入手分析源码:
1) 如何自定义缓存线程和加载线程的配置;
2) 缓存设计算法。

首先看第一个问题,要看ImagePipeline的配置,我们来分析一下ImagePipelineConfig的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Nullable private final AnimatedImageFactory mAnimatedImageFactory;
private final Bitmap.Config mBitmapConfig;
private final Supplier<MemoryCacheParams> mBitmapMemoryCacheParamsSupplier; //内存缓存数据的策略
private final CacheKeyFactory mCacheKeyFactory; //缓存键值对的获取
private final Context mContext;
private final boolean mDownsampleEnabled;
private final boolean mDecodeMemoryFileEnabled;
private final FileCacheFactory mFileCacheFactory; // 文件缓存键值对
private final Supplier<MemoryCacheParams> mEncodedMemoryCacheParamsSupplier; //原码内存缓存参数
private final ExecutorSupplier mExecutorSupplier; //获取线程池
private final ImageCacheStatsTracker mImageCacheStatsTracker;//Cache埋点工具
@Nullable private final ImageDecoder mImageDecoder; //解码器
private final Supplier<Boolean> mIsPrefetchEnabledSupplier;
private final DiskCacheConfig mMainDiskCacheConfig;//磁盘缓存配置
private final MemoryTrimmableRegistry mMemoryTrimmableRegistry;
private final NetworkFetcher mNetworkFetcher; //网络获取器
@Nullable private final PlatformBitmapFactory mPlatformBitmapFactory;
private final PoolFactory mPoolFactory;
private final ProgressiveJpegConfig mProgressiveJpegConfig; //渐进图片配置
private final Set<RequestListener> mRequestListeners;
private final boolean mResizeAndRotateEnabledForNetwork;
private final DiskCacheConfig mSmallImageDiskCacheConfig;//小图缓存配置
private final ImagePipelineExperiments mImagePipelineExperiments;
...

ImagePipeline的可配置项如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ImagePipelineConfig config = ImagePipelineConfig.newBuilder()
.setBitmapMemoryCacheParamsSupplier(bitmapCacheParamsSupplier) //bitmap缓存配置
.setCacheKeyFactory(cacheKeyFactory) //设置缓存键值对
.setEncodedMemoryCacheParamsSupplier(encodedCacheParamsSupplier)//设置原码内存缓存配置
.setExecutorSupplier(executorSupplier) //各种线程池
.setImageCacheStatsTracker(imageCacheStatsTracker)//缓存打点
.setMainDiskCacheConfig(mainDiskCacheConfig) //主磁盘缓存
.setMemoryTrimmableRegistry(memoryTrimmableRegistry)
.setNetworkFetchProducer(networkFetchProducer)//网络请求配置
.setPoolFactory(poolFactory)
.setProgressiveJpegConfig(progressiveJpegConfig)//渐进图片配置
.setRequestListeners(requestListeners)//请求监听
.setSmallImageDiskCacheConfig(smallImageDiskCacheConfig)//小图缓存
.build();
Fresco.initialize(context, config);

ImagePipeline用到了三个缓存,首先是DiskCache,然后还有两个MemoryCache,分别是保存已解码Bitmap的和保存EncodedImage的缓存。Fresco将未解码的原始数据也进行了内存缓存,然后根据是否旋转或者缩放以及解码质量进行解码成bitmap存放内存空间,其实在我所接触的应用场景中这部分内容其实是不太需要的,因为一张图片基本上只在一个地方使用,即使多处使用也不太需要这么复杂的变换,可能Fresco想的比较周到吧。
内存缓存使用的是通用的lru算法(最近最少使用原则),内存缓存的设计代码在CountingMemoryCache,CountingMemoryCache是一个基于LRU策略来管理缓存中元素的一个类,它实现的trim()方法可以根据Type的不同来采取不同策略的回收为:

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
/**
* Layer of memory cache stack responsible for managing eviction of the the cached items.
*
* <p> This layer is responsible for LRU eviction strategy and for maintaining the size boundaries
* of the cached items.
*
* <p> Only the exclusively owned elements, i.e. the elements not referenced by any client, can be
* evicted.
*
* @param <K> the key type
* @param <V> the value type
*/
@ThreadSafe
public class CountingMemoryCache<K, V> implements MemoryCache<K, V>, MemoryTrimmable {
...//省略代码
/** Trims the cache according to the specified trimming strategy and the given trim type. */
@Override
public void trim(MemoryTrimType trimType) {
ArrayList<Entry<K, V>> oldEntries;
final double trimRatio = mCacheTrimStrategy.getTrimRatio(trimType);
synchronized (this) {
int targetCacheSize = (int) (mCachedEntries.getSizeInBytes() * (1 - trimRatio));
int targetEvictionQueueSize = Math.max(0, targetCacheSize - getInUseSizeInBytes());
oldEntries = trimExclusivelyOwnedEntries(Integer.MAX_VALUE, targetEvictionQueueSize);
makeOrphans(oldEntries);
}
maybeClose(oldEntries);
maybeNotifyExclusiveEntryRemoval(oldEntries);
maybeUpdateCacheParams();
maybeEvictEntries();
}
...//省略代码
}

Fresco使用的黑科技还有很多,它是一份巨大的宝藏等着挖掘,我只是粗浅的总结了部分我get到的点,以后进一步深入学习中再和大家分享。

Fresco源码浅析-Drawee模块(二)

发表于 2016-09-29

Drawee模块负责图片的展示,主要涉及到的概念包括DraweeView(V)、DraweeHierarchy(M)、DraweeController(C),这是一个典型的MVC的结构。

DraweeView(V)

使用Fresco的时候,我们首先直接使用SimpleDraweeView这个官方自定义的控件,它的继承结构是这样的:
SimpleDraweeView类继承关系.png

SimpleDraweeView主要实现了setImageURI方法,设置了一个controller。

1
2
3
4
5
6
7
8
public void setImageURI(Uri uri, @Nullable Object callerContext) {
DraweeController controller = mSimpleDraweeControllerBuilder
.setCallerContext(callerContext)
.setUri(uri)
.setOldController(getController())
.build();
setController(controller);
}

GenericDraweeView继承自DraweeView,实现了一个GenericDraweeHierarchy的泛型。Fresco支持高度定制,你可以通过重写DraweeHierarchy定制自己的数据处理。
DraweeView控制图片展示的核心业务逻辑,控件初始化的时候初始化了一个DraweeHolder帮助类,并将得到的Hierarchy以及Controller交给DraweeHolder处理。

DraweeHierarchy(M)

Fresco实现了各种数据处理场景下界面的展示,包括:

  • 占位图
  • 自身图片
  • 进度
  • 错误图
  • 重试
  • …
    DraweeHierarchy作为model主要组织这些数据。GenericDraweeHierarchy实现了一个默认的DraweeHierarchy,因为展示的图片数据都是Drawable,针对这些场景,DraweeHierarchy实现了一个FadeDrawable类,继承自ArrayDrawable,从名字可以看出这是一个层级的Drawable,然后通过Fade某一层Alpha值,控制显示。成员变量如下:
    1
    2
    3
    4
    5
    6
    7
    8
    private final RootDrawable mTopLevelDrawable; //Imageview最终绘制需要的Drawable。
    private final FadeDrawable mFadeDrawable; //所有数据整合后存放的ArrayDrawable。
    private final ForwardingDrawable mActualImageWrapper; //加载成功展示的图片
    private final int mPlaceholderImageIndex;//占位图index
    private final int mProgressBarImageIndex;//进度条index
    private final int mActualImageIndex;//图片index
    private final int mRetryImageIndex;//重新加载index
    private final int mFailureImageIndex;//失败图index

在DraweeHierarchy构造的时候填充mFadeDrawable:

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
mActualImageWrapper = new ForwardingDrawable(mEmptyActualImageDrawable);
int numBackgrounds = (builder.getBackgrounds() != null) ? builder.getBackgrounds().size() : 0;
int numOverlays = (builder.getOverlays() != null) ? builder.getOverlays().size() : 0;
numOverlays += (builder.getPressedStateOverlay() != null) ? 1 : 0;
// layer indices and count
int numLayers = 0;
int backgroundsIndex = numLayers;
numLayers += numBackgrounds;
mPlaceholderImageIndex = numLayers++;
mActualImageIndex = numLayers++;
mProgressBarImageIndex = numLayers++;
mRetryImageIndex = numLayers++;
mFailureImageIndex = numLayers++;
int overlaysIndex = numLayers;
numLayers += numOverlays;
// array of layers
Drawable[] layers = new Drawable[numLayers];
if (numBackgrounds > 0) {
int index = 0;
for (Drawable background : builder.getBackgrounds()) {
layers[backgroundsIndex + index++] = buildBranch(background, null);
}
}
layers[mPlaceholderImageIndex] = buildBranch(
builder.getPlaceholderImage(),
builder.getPlaceholderImageScaleType());
layers[mActualImageIndex] = buildActualImageBranch(
mActualImageWrapper,
builder.getActualImageScaleType(),
builder.getActualImageFocusPoint(),
builder.getActualImageMatrix(),
builder.getActualImageColorFilter());
layers[mProgressBarImageIndex] = buildBranch(
builder.getProgressBarImage(),
builder.getProgressBarImageScaleType());
layers[mRetryImageIndex] = buildBranch(
builder.getRetryImage(),
builder.getRetryImageScaleType());
layers[mFailureImageIndex] = buildBranch(
builder.getFailureImage(),
builder.getFailureImageScaleType());
if (numOverlays > 0) {
int index = 0;
if (builder.getOverlays() != null) {
for (Drawable overlay : builder.getOverlays()) {
layers[overlaysIndex + index++] = buildBranch(overlay, null);
}
}
if (builder.getPressedStateOverlay() != null) {
layers[overlaysIndex + index] = buildBranch(builder.getPressedStateOverlay(), null);
}
}

DraweeHierarchy 对于属性的设置利用了构造者模式,定义了一个GenericDraweeHierarchyBuilder。GenericDraweeHierarchyBuilder可以直接在代码里初始化,也可以读取XML的配置信息。

DraweeController(C)

DraweeController的功能包括:

  • 发起数据请求,得到请求结果,刷新界面;
  • 针对界面行为事件控制数据流的处理逻辑。
    controller 的类结构:
    DraweeController
    –| AbstractDraweeController
    —-| PipelineDraweeController

在纷繁的代码中追踪数据加载的逻辑确实好头疼,我发现单纯的从drawee来查看数据的加载过程还是太复杂,我将在imagepipeline模块整理完之后再回头整理drawee与image pipeline的交互过程,今天我们只是简单的看一下流程。
我们只能从最开始的view初始化看起,DraweeView的onAttachedToWindow函数中调用了attachController方法:

1
2
3
4
5
6
7
8
9
10
11
private void attachController() {
if (mIsControllerAttached) {
return;
}
mEventTracker.recordEvent(Event.ON_ATTACH_CONTROLLER);
mIsControllerAttached = true;
if (mController != null &&
mController.getHierarchy() != null) {
mController.onAttach();
}
}

这里就是view调用controller数据加载的入口。controller的onAttach方法又做了什么呢:

1
2
3
4
5
6
7
8
9
10
@Override
public void onAttach() {
mEventTracker.recordEvent(Event.ON_ATTACH_CONTROLLER);
Preconditions.checkNotNull(mSettableDraweeHierarchy);
mDeferredReleaser.cancelDeferredRelease(this);
mIsAttached = true;
if (!mIsRequestSubmitted) {
submitRequest(); //发起请求
}
}

看字面含义我们已经猜到,他在最后发起了数据的请求,我们再看看这个请求是怎么做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void submitRequest() {
...........//省略代码
mDataSource = getDataSource();
...........//省略代码
final DataSubscriber<T> dataSubscriber =
new BaseDataSubscriber<T>() {
@Override
public void onNewResultImpl(DataSource<T> dataSource) {
..............//获取图片成功
}
@Override
public void onFailureImpl(DataSource<T> dataSource) {
...............//获取图片失败
}
@Override
public void onProgressUpdate(DataSource<T> dataSource) {
..............//图片获取过程中
}
};
mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor);
}

我们看到这里利用了观察者的设计模式,得到了一个数据流,并向这个数据流注册了一个观察者。getDataSource是个虚方法,真正的实现在PipelineDraweeController和VolleyDraweeController中,默认调用Fresco的initialize方法,创建的是PipelineDraweeController,他的getDataSource实现如下:

1
2
3
4
@Override
protected DataSource<CloseableReference<CloseableImage>> getDataSource() {
return mDataSourceSupplier.get();
}

Supplier的用法我还是第一次看到,他有一个官方的名字叫做“惰性求值”,我们传递Supplier对象,直到调用get方法时,运算才会执行。我们查看get方法的重载在AbstractDraweeControllerBuilder看到了这样一块代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected Supplier<DataSource<IMAGE>> getDataSourceSupplierForRequest(
final REQUEST imageRequest,
final boolean bitmapCacheOnly) {
final Object callerContext = getCallerContext();
return new Supplier<DataSource<IMAGE>>() {
@Override
public DataSource<IMAGE> get() {
return getDataSourceForRequest(imageRequest, callerContext, bitmapCacheOnly);
}
@Override
public String toString() {
return Objects.toStringHelper(this)
.add("request", imageRequest.toString())
.toString();
}
};
}

没错,就是getDataSourceForRequest实现了数据的加载,我们在PipelineDraweeControllerBuilder中看到了具体实现:

1
2
3
4
5
6
7
8
9
10
11
@Override
protected DataSource<CloseableReference<CloseableImage>> getDataSourceForRequest(
ImageRequest imageRequest,
Object callerContext,
boolean bitmapCacheOnly) {
if (bitmapCacheOnly) {
return mImagePipeline.fetchImageFromBitmapCache(imageRequest, callerContext);
} else {
return mImagePipeline.fetchDecodedImage(imageRequest, callerContext);
}
}

至此我们终于调用了ImagePipeline的fetch方法,实现了数据的请求,我们回过头来总结一下这个流程:
ImageView在setImageUri的过程过程中创建了一个controller,controller的创建基于builder模式,最后调用build函数,接下来的函数链是:buildController -> obtainController -> obtainDataSourceSupplier -> getDataSourceSupplierForRequest完成了整个controller的创建,接下来在View的onAttachedToWindow调用controller中依次调用 onAttach -> submitRequest -> getDataSource 然后执行get中的请求操作。

Controller中同时定义了一个GestureDetector,用于对View事件的拦截,我看到主要还是在onClick事件中处理是否重新加载。

至此整个图片展示的部分我们已经大致的梳理了一遍,核心还是在于model的处理,ImageView设置了一个drawable之后,control直接控制这个drawable,利用drawable的invalidateSelf函数直接刷新View,MVC模式中model是可以操作View的,这也真是MVC与MVP模式的最大区别。controller的逻辑比较简单,就是先初始化构建,然后View显示的时候执行他的数据请求操作,但是代码比较复杂,梳理起来比较麻烦,还好在梳理的过程中看到了很多优秀的设计。

下一页 ImagePipeline介绍

Fresco源码浅析-序(一)

发表于 2016-09-29

大名鼎鼎的Fresco开源一年多了,已经有越来越多的尝鲜者开始使用。目前我们的项目对于图片的加载没有过多的需求,但对于Fresco还是充满了好。我接下来几篇博文讲围绕着他的架构设计、图片处理、缓存设计、线程管理等议题深入源码,尽可能的给出自己的一些理解。(水平有限,第一次尝试写博客,有不对的地方欢迎指正)

Fresco优势

  • 内存管理
    bitmap是Android中内存占用的大块头,Fresco用了一个略显“流氓”的方案,将bitmap存储在共享内存区域,这样就不会计算在app分配的堆内存大小了,有效的减少了OOM,但这毕竟是钻了Android的一个空子,5.0以上的版本他及时更改了过来。不过这也可以作为一种思路,学习一下如何高效利用共享内存,以备我们随时“流氓”一下^_^
  • 支持更多的图片格式
    Fresco支持本地图片和网络图片,支持GIF和WebP格式的图片,
  • 渐进式图片加载
    渐进式图片能带来更好的用户体验,Fresco支持这个格式,而且使用的时候只需要像普通图片一样提供Url即可,不过这还需要后端哥哥们的配合。。。
  • 封装了各种场景下的图片展示

    圆角,自定义居中,占位图,错误图,loading图,再次加载图,你能想到的场景Fresco都帮你想到了,使用起来及其方便。

    源码分析要点

    github上down下来的Fresco代码结构是这样的:
    Fresco官方代码结构.png
    看到这么多module是不是有点爆炸的感觉,反正我第一眼是炸了。。
    然后我在另外一个工程引入了一下Fresco包,给我下载的代码结构是这样的:
    Fresco引入包代码结构.png
    对比看起来就比较清晰了,主要是Facebook的core包,drawee和imagepipeline。core包是Facebook的基础包,这里面有很多工具类可以借用,drawee主要是用来图片的展示,imagepipeline则设计了一个图片加载框架,接下来讲主要围绕这两块深入到Fresco的源码中去(一直在尝试深入,其实只是在门口游荡,惭愧)。

  • drawee

  • imagepipeline

简单使用

Fresco使用起来特别简单,如果你是已经在代码中使用了大量网络图片,那改动可能会有点大,因为view需要改用Fresco默认提供的一个叫SimpleDraweeView的ImageView,这要涉及到xml和Java代码的改动。不过使用起来确实超级方便,直接调用setimageuri方法就完事了,更多的关于初始化设置占位图、错误图、loading图、重复加载图详见官方说明,这里不再赘述了(有时间我准备改造一下Fresco的使用方式,还是用我们熟悉的传入ImageView和Uri的方式,这样集成和替换图片加载库的代价就会很低,如果觉得有价值请顶我,让我更有动力)。

文献引用

Fresco 中文文档
Fresco 源码解析

下一页 Drawee模块介绍

wankunpan

wankunpan

6 日志
© 2016 wankunpan
由 Hexo 强力驱动
主题 - NexT.Muse