MVC & MVP & MVVM

#MVC & MVP & MVVP

先放2个引用,概念上的讨论我觉得可以参考这2个。

http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html

http://weibo.com/p/1001603808855434892996

我主要想讨论下 这一演化过程,和架构选择的必要性。

###MVC

首先,传统的MVC项目中,layout.xml承担View的角色,Activity承担Controller的角色。

这本身没有什么问题,如果你的Activity代码不足千行(当然不包含网络请求及解析),多拆分几个方法,写好注释,也可以一用。
你也可以通过把其中部分逻辑 模块化抽出的方式 简化Activity代码。很多小型项目或者功能简单的Activity,使用MVC模型就可以了,盲目的使用MVP或MVVM并不是好事。

###MVP

然而随着项目的不断发展和复杂化,Activity承担的内容越来越多,请求数据,响应数据,数据处理,生成UI,修改view的状态,特效实现。

当Activity过于膨胀,我们就需要拆分它。

按职责,拆分为 业务逻辑UI逻辑,就是Android中当前推荐的MVP实现.

  • 业务逻辑

    包含请求数据,响应操作,对数据的处理等等业务相关的代码。

  • UI逻辑

    包含界面的调整,UI状态的变化,动效等。

业务逻辑通过抽出 Presenter 层实现。
UI逻辑通过构造 ViewInterface并由Activity实现。

由于View和Presenter仅通过 ViewInterface关联,单独修改View或Presenter不会互相影响。

这是个好事,一般工程上小的UI调整总是多如牛毛,避免其影响到业务逻辑层可以减少很多bug的产生。

反过来也是一样。

当然,坏处也有,当你的Presenter需要添加一个新的功能/行为时,你需要在ViewInterface上添加新的接口,并在Activity上实现。
更多的灵活性总是意味着 更复杂的项目层次。

如果你遇到2种问题,那么你应当使用MVP

  1. 你需要自动化测试。

MVP的引入会让测试变得非常方便。
写一个简单的包含mock数据的Presenter就可以测试所有UI逻辑。
可以非常方便的在没有后端的情况下完成所有UI层逻辑及测试

同时 Presenter的测试也变得容易,因为Presenter现在与View是完全分离的,这意味着,测试业务逻辑时,你甚至不需要Android 环境,而且可以简单的mock ui行为。
这可比用脚本在模拟器上跑测试要方便多了。

  1. 更高的复用率

Activity或Presenter都可以复用。如果你的项目需要快速迭代,那往往意味着,大量的UI变更,大量的逻辑调整。
分离Activity和Presenter可以减少很多 因为互相影响产生的bug,而且页面和逻辑变得方便调整和复用。

###MVVM

Android上谈MVVM,目前大多是指data binding.
这个东西刚出来的时候还比较火,但是实际用起来坑还比较多,目前没有看到哪个大型项目有实践这个东西。

具体使用可以参考这个文档 MasteringAndroidDataBinding
看起来不错,用起来都是坑。

Weak Handler 与 内存泄露

#Weak Handler 与 内存泄露

Handler使用不当比较容易造成内存泄露.

比如这个例子:http://www.jianshu.com/p/c49f778e7acf

通常的原因就是 Handler的生命周期和Activity的生命周期不一致.


一个通用的场景是 使用 匿名内部类 实例 作为某个 行为/动作的 回调,如果该行为/动作 是异步的,则其返回时间往往无法确定,有造成内存泄露风险.

使用静态内部类,或者妥善处理生命周期,都不会造成内存泄露,反过来,当没有内存泄露风险时,一般直接匿名内部类即可.

这其实是一个特别矛盾的说法.

因为这要求程序员能 能了解到 回调行为是在何时发生的.

而相反,我们设计接口回调的时候总是尽力屏蔽内部实现细节.

而面对不确定的行为,当然也可以使用静态内部类,或者removeCallback去取消回调(特指Handler),然而这样代码变得繁琐,或者添加不必要的代码.

对于Handler,一个简单的第三方解决方案是使用 android-weak-handler ,该库尝试使用WeakReference解决这个问题,然而却引入新的问题.

https://github.com/badoo/android-weak-handler

核心代码如下:

public class WeakHandler {
    private final Handler.Callback mCallback; // hard reference to Callback. We need to keep callback in memory
    private final ExecHandler mExec;
    private Lock mLock = new ReentrantLock();
    @SuppressWarnings("ConstantConditions")
    @VisibleForTesting
    final ChainedRef mRunnables = new ChainedRef(mLock, null);

    public WeakHandler() {
        mCallback = null;
        mExec = new ExecHandler();
    }

    public WeakHandler(@Nullable Handler.Callback callback) {
        mCallback = callback; // Hard referencing body
        mExec = new ExecHandler(new WeakReference<>(callback)); // Weak referencing inside ExecHandler
    }

    public WeakHandler(@NonNull Looper looper) {
        mCallback = null;
        mExec = new ExecHandler(looper);
    }

    public WeakHandler(@NonNull Looper looper, @NonNull Handler.Callback callback) {
        mCallback = callback;
        mExec = new ExecHandler(looper, new WeakReference<>(callback));
    }

    public final boolean post(@NonNull Runnable r) {
        return mExec.post(wrapRunnable(r));
    }

    public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) {
        return mExec.postAtTime(wrapRunnable(r), uptimeMillis);
    }

    public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) {
        return mExec.postAtTime(wrapRunnable(r), token, uptimeMillis);
    }

    public final boolean postDelayed(Runnable r, long delayMillis) {
        return mExec.postDelayed(wrapRunnable(r), delayMillis);
    }

    public final boolean postAtFrontOfQueue(Runnable r) {
        return mExec.postAtFrontOfQueue(wrapRunnable(r));
    }

    public final void removeCallbacks(Runnable r) {
        final WeakRunnable runnable = mRunnables.remove(r);
        if (runnable != null) {
            mExec.removeCallbacks(runnable);
        }
    }

    public final void removeCallbacks(Runnable r, Object token) {
        final WeakRunnable runnable = mRunnables.remove(r);
        if (runnable != null) {
            mExec.removeCallbacks(runnable, token);
        }
    }

    public final boolean sendMessage(Message msg) {
        return mExec.sendMessage(msg);
    }

    public final boolean sendEmptyMessage(int what) {
        return mExec.sendEmptyMessage(what);
    }

    public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
        return mExec.sendEmptyMessageDelayed(what, delayMillis);
    }

    public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {
        return mExec.sendEmptyMessageAtTime(what, uptimeMillis);
    }

    public final boolean sendMessageDelayed(Message msg, long delayMillis) {
        return mExec.sendMessageDelayed(msg, delayMillis);
    }

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        return mExec.sendMessageAtTime(msg, uptimeMillis);
    }

    public final boolean sendMessageAtFrontOfQueue(Message msg) {
        return mExec.sendMessageAtFrontOfQueue(msg);
    }

    public final void removeMessages(int what) {
        mExec.removeMessages(what);
    }

    public final void removeMessages(int what, Object object) {
        mExec.removeMessages(what, object);
    }

    public final void removeCallbacksAndMessages(Object token) {
        mExec.removeCallbacksAndMessages(token);
    }

    public final boolean hasMessages(int what) {
        return mExec.hasMessages(what);
    }

    public final boolean hasMessages(int what, Object object) {
        return mExec.hasMessages(what, object);
    }

    public final Looper getLooper() {
        return mExec.getLooper();
    }

    private WeakRunnable wrapRunnable(@NonNull Runnable r) {
        //noinspection ConstantConditions
        if (r == null) {
            throw new NullPointerException("Runnable can't be null");
        }
        final ChainedRef hardRef = new ChainedRef(mLock, r);
        mRunnables.insertAfter(hardRef);
        return hardRef.wrapper;
    }

    private static class ExecHandler extends Handler {
        private final WeakReference<Handler.Callback> mCallback;

        ExecHandler() {
            mCallback = null;
        }

        ExecHandler(WeakReference<Handler.Callback> callback) {
            mCallback = callback;
        }

        ExecHandler(Looper looper) {
            super(looper);
            mCallback = null;
        }

        ExecHandler(Looper looper, WeakReference<Handler.Callback> callback) {
            super(looper);
            mCallback = callback;
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            if (mCallback == null) {
                return;
            }
            final Handler.Callback callback = mCallback.get();
            if (callback == null) { // Already disposed
                return;
            }
            callback.handleMessage(msg);
        }
    }

    static class WeakRunnable implements Runnable {
        private final WeakReference<Runnable> mDelegate;
        private final WeakReference<ChainedRef> mReference;

        WeakRunnable(WeakReference<Runnable> delegate, WeakReference<ChainedRef> reference) {
            mDelegate = delegate;
            mReference = reference;
        }

        @Override
        public void run() {
            final Runnable delegate = mDelegate.get();
            final ChainedRef reference = mReference.get();
            if (reference != null) {
                reference.remove();
            }
            if (delegate != null) {
                delegate.run();
            }
        }
    }

    static class ChainedRef {
        @Nullable
        ChainedRef next;
        @Nullable
        ChainedRef prev;
        @NonNull
        final Runnable runnable;
        @NonNull
        final WeakRunnable wrapper;

        @NonNull
        Lock lock;

        public ChainedRef(@NonNull Lock lock, @NonNull Runnable r) {
            this.runnable = r;
            this.lock = lock;
            this.wrapper = new WeakRunnable(new WeakReference<>(r), new WeakReference<>(this));
        }

        public WeakRunnable remove() {
            lock.lock();
            try {
                if (prev != null) {
                    prev.next = next;
                }
                if (next != null) {
                    next.prev = prev;
                }
                prev = null;
                next = null;
            } finally {
                lock.unlock();
            }
            return wrapper;
        }

        public void insertAfter(@NonNull ChainedRef candidate) {
            lock.lock();
            try {
                if (this.next != null) {
                    this.next.prev = candidate;
                }

                candidate.next = this.next;
                this.next = candidate;
                candidate.prev = this;
            } finally {
                lock.unlock();
            }
        }

        @Nullable
        public WeakRunnable remove(Runnable obj) {
            lock.lock();
            try {
                ChainedRef curr = this.next; // Skipping head
                while (curr != null) {
                    if (curr.runnable == obj) { // We do comparison exactly how Handler does inside
                        return curr.remove();
                    }
                    curr = curr.next;
                }
            } finally {
                lock.unlock();
            }
            return null;
        }
    }
}

这个类的实现有个问题,为了避免持有Runnable的引用,使用WeakRunnable作为Wrapper,为了能够在Activity销毁时释放Runnable,这里使用的WeakReference去持有Runnable引用.
从目的上看,它确实解决了由于Runnable隐式持有Activity强引用而导致Acticity实例无法销毁的问题

##但是

作为匿名内部类被传入的Runnable对象如果只存在一个WeakReference,那意味着,任何一次GC操作都将导致其被回收.
看看这个例子

import com.badoo.mobile.util.WeakHandler;

public class ExampleActivity extends Activity {

    private WeakHandler mHandler; // We still need at least one hard reference to WeakHandler

    protected void onCreate(Bundle savedInstanceState) {
        mHandler = new WeakHandler();
        ...
    }

    private void onClick(View view) {
        mHandler.postDelayed(new Runnable() {
            view.setVisibility(View.INVISIBLE);
        }, 5000);
    }
}

如果在这5s内系统触发了一次GC操作会怎么样? Runnable会被回收?

将Demo简单修改一下

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    WeakHandler  mHandler = new WeakHandler();
    mHandler.postDelayed(new Runnable() {


        @Override
        public void run() {
            System.out.println("####################### Do Something");
            );
        }
    }, 10000);

    mHandler.postDelayed(new Runnable() {


        @Override
        public void run() {
            System.out.println("#######################GC");

            System.gc();
        }
    }, 5000);

}

看看运行结果,GC操作发生了,而Do Something并没有执行,因为 GC动作发生时, WeakReference持有的Runnable已经被释放了.

通常来讲,GC操作发生的时间不可预料的,你无法预期等待回调的这几秒种内用户做了什么操作,而一旦触发了GC,则回调会被立刻取消

注意,Activity并未被关闭,Runnable回调也会被释放!

如果在大型项目中遇到这样的问题,排查一定是灾难性的.因为GC的不确定性,出现了回调丢失情况一定是随机的,难以预测的,难以重现的.

而处理这个问题的办法是,将mHandler从临时变量 换成 成Activity的一个实例变量.

然而通常程序员不一定意识这2种写法的区别,而且使用系统提供的Handler时,这2种写法是没有区别的.

最后我检查了下文档,作者还是写了一句话作为提示

We still need at least one hard reference to WeakHandler

至于使用者是否能意识到问题所在我持怀疑态度.

第三方库就是容易出现这种问题,使用起来能很方便的解决很多问题,然而又很容易挖几个隐蔽的坑给你.

PreLoadHack

Android换肤技术 PreLoad Hack

参考 Android换肤技术总结

内部资源加载方案 大同小异,而且使用和实现缺陷非常多,实际使用价值不大.

  • 对于复杂的皮肤,需要太多的设置.

  • 对于简单的皮肤(类似白天/黑夜/关灯模式),有更简单的实现方式

主要来看动态加载方案

##resource替换
开源项目可参照Android-Skin-Loader

可以参考顶上的Blog链接

实现机制其实其实和遍历RootView的方案区别不大,这个是标记Skin enable后,遍历标记的view

遍历所有SkinItem,遍历SkinAttr,然后调用skinAtrr.apply(view)方法设置属性

这项目优点有2个:

  • 相比于遍历RootView的粗暴实现,这个实现划分层次更清晰

  • 将资源打包成apk,然后通过AssetManager加载

PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;

AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);

Resources superRes = context.getResources();
Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());

实现机制ZYF写了2句,但是我感觉不是很对.


自己整理一下详细实现机制如下:

Android-Skin-Loader并没有覆盖application的getResource方法.

  • 使用时必须BaseActivity

  • onCreate的时候调用 getLayoutInflater().setFactory(mSkinInflaterFactory);

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mSkinInflaterFactory = new SkinInflaterFactory();
    getLayoutInflater().setFactory(mSkinInflaterFactory);
}
  • 于是View被Inflater创建的时候会经过mSkinInflaterFactory的onCreateView方法

  • 在mSkinInflaterFactory的onCreateView方法中,获取所有相关的属性,保存到SkinItem数组中

  • 换肤的时候会调用BaseActivity的onThemeUpdate方法

  • onThemeUpdate方法中遍历所有SkinItem并调用apply方法修改参数

public void apply(View view) {        
    if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){
        view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId));
    }else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){
        Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);
        view.setBackground(bg);
    }
}
  • 此时调用的getColor和getDrawable会通过AssetManager加载指定apk中的资源

整个流程中没有哪里经过Application,只是通过AssetManager加载了另一个apk中的Resource.

比遍历RootView好一点的就是它是通过LayoutInflater的Factory去检查每个View是否需要SkinUpdate功能,然后将需要的View保存下来,ThemeUpdate的时候只刷新这些View.
性能上应当比遍历RootView高效一些吧.

Hack Resources internally

引用自ZYF的Blog

黑科技方法,直接对Resources进行hack,Resources.java:

// Information about preloaded resources.  Note that they are not
// protected by a lock, because while preloading in zygote we are all
// single-threaded, and after that these are immutable.
private static final LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables;
private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables
        = new LongSparseArray<Drawable.ConstantState>();
private static final LongSparseArray<ColorStateList> sPreloadedColorStateLists
        = new LongSparseArray<ColorStateList>();

直接对Resources里面的这三个LongSparseArray进行替换,由于apk运行时的资源都是从这三个数组里面加载的,所以只要采用interceptor模式:
自己实现一个LongSparseArray,并通过反射set回去,就能实现换肤,具体getDrawable等方法里是怎么取preload数组的,可以自己看Resources的源码。
等等,就这么简单?,NONO,少年你太天真了,怎么去加载xml,9patch的padding怎么更新,怎么打包/加载自定义的皮肤包,drawable的状态怎么刷新,等等。这些都是你需要考虑的,在存在插件的app中,还需要考虑是否会互相覆盖resource id的问题,进而需要修改apt,把resource id按位放在2个range。
手Q和独立版QQ空间使用的是这种方案,效果挺好。


这方案也没个具体说明,就一句 自己实现一个LongSparseArray ,真的是蛋碎.
不过有个提示也是好的.

首先反射一下该字段看看读出来什么东西

Resources resource = getApplicationContext().getResources();

try {
Field field =Resources.class.getDeclaredField("sPreloadedDrawables");
field.setAccessible(true);

LongSparseArray<Drawable.ConstantState>[]    sPreloadedDrawables = (LongSparseArray<Drawable.ConstantState>[] )field.get(resource);

for (LongSparseArray<Drawable.ConstantState> s:sPreloadedDrawables)
    for (int i = 0; i < s.size(); i++) {
        System.out.println(s.valueAt(i));
    }
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (NoSuchFieldException e) {
    e.printStackTrace();
}

...
10-28 20:15:54.105 24502-24502/com.dk_exp.preloadhack I/System.out: android.graphics.drawable.LayerDrawable$LayerState@8197fd4
10-28 20:15:54.105 24502-24502/com.dk_exp.preloadhack I/System.out: android.graphics.drawable.LayerDrawable$LayerState@9c6357d
10-28 20:15:54.105 24502-24502/com.dk_exp.preloadhack I/System.out: android.graphics.drawable.LayerDrawable$LayerState@95f0c72
10-28 20:15:54.105 24502-24502/com.dk_exp.preloadhack I/System.out: android.graphics.drawable.StateListDrawable$StateListState@d78c540
10-28 20:15:54.105 24502-24502/com.dk_exp.preloadhack I/System.out: android.graphics.drawable.StateListDrawable$StateListState@805ac79
10-28 20:15:54.105 24502-24502/com.dk_exp.preloadhack I/System.out: android.graphics.drawable.BitmapDrawable$BitmapState@38d7dbe
10-28 20:15:54.105 24502-24502/com.dk_exp.preloadhack I/System.out: android.graphics.drawable.StateListDrawable$StateListState@e829e1f
10-28 20:15:54.105 24502-24502/com.dk_exp.preloadhack I/System.out: android.graphics.drawable.StateListDrawable$StateListState@4c4f56c
10-28 20:15:54.105 24502-24502/com.dk_exp.preloadhack I/System.out: android.graphics.drawable.VectorDrawable$VectorDrawableState@82b9735
10-28 20:15:54.105 24502-24502/com.dk_exp.preloadhack I/System.out: android.graphics.drawable.VectorDrawable$VectorDrawableState@a9fb7ca
...

可以看到sPreloadedDrawables里持有大量的State对象,比如BitmapDrawable$BitmapState

作为BitmapDrawable的内部类,还是比较简单的,贴一下完整代码

final static class BitmapState extends ConstantState {
    final Paint mPaint;

    // Values loaded during inflation.
    int[] mThemeAttrs = null;
    Bitmap mBitmap = null;
    ColorStateList mTint = null;
    Mode mTintMode = DEFAULT_TINT_MODE;
    int mGravity = Gravity.FILL;
    float mBaseAlpha = 1.0f;
    Shader.TileMode mTileModeX = null;
    Shader.TileMode mTileModeY = null;
    int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
    boolean mAutoMirrored = false;

    int mChangingConfigurations;
    boolean mRebuildShader;

    BitmapState(Bitmap bitmap) {
        mBitmap = bitmap;
        mPaint = new Paint(DEFAULT_PAINT_FLAGS);
    }

    BitmapState(BitmapState bitmapState) {
        mBitmap = bitmapState.mBitmap;
        mTint = bitmapState.mTint;
        mTintMode = bitmapState.mTintMode;
        mThemeAttrs = bitmapState.mThemeAttrs;
        mChangingConfigurations = bitmapState.mChangingConfigurations;
        mGravity = bitmapState.mGravity;
        mTileModeX = bitmapState.mTileModeX;
        mTileModeY = bitmapState.mTileModeY;
        mTargetDensity = bitmapState.mTargetDensity;
        mBaseAlpha = bitmapState.mBaseAlpha;
        mPaint = new Paint(bitmapState.mPaint);
        mRebuildShader = bitmapState.mRebuildShader;
        mAutoMirrored = bitmapState.mAutoMirrored;
    }

    @Override
    public boolean canApplyTheme() {
        return mThemeAttrs != null || mTint != null && mTint.canApplyTheme();
    }

    @Override
    public int addAtlasableBitmaps(Collection<Bitmap> atlasList) {
        if (isAtlasable(mBitmap) && atlasList.add(mBitmap)) {
            return mBitmap.getWidth() * mBitmap.getHeight();
        }
        return 0;
    }

    @Override
    public Drawable newDrawable() {
        return new BitmapDrawable(this, null);
    }

    @Override
    public Drawable newDrawable(Resources res) {
        return new BitmapDrawable(this, res);
    }

    @Override
    public int getChangingConfigurations() {
        return mChangingConfigurations
                | (mTint != null ? mTint.getChangingConfigurations() : 0);
    }
}

由于已经反射获得了sPreloadedDrawables ,那么想办法修改sPreloadedDrawables里的对象应当就可以修改 图片 资源了.

然而出现了多个问题

  • 由于BitmapState在类外无法访问,抽象类Drawable.ConstantState又没有提供修改的接口.

  • 稀疏数组的key并不是ResourceId

key = (((long) value.assetCookie) << 32) | value.data;

追踪一下调用栈,这个value对象来自一个native方法,暂时不方便获得assetCookie和data的计算方法

private native final int loadResourceValue(int ident, short density, TypedValue outValue,
        boolean resolve);

不过Resource本身提供getVaklue方法来给TypeValue填充数据

public void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)

那么我可以尝试直接通过TypeValue来读出preload中的数据

TypedValue value = new TypedValue();
resource.getValue(R.drawable.charming,value,true );

long  key = -1;
if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
        && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
    key = value.data;
} else {
    key = (((long) value.assetCookie) << 32) | value.data;
}
Drawable.ConstantState cs =sPreloadedDrawable.get(key);

然而这里遇到了一个问题,获取TypeValue和计算key都很正常

但是通过key获取ConstantState返回了null.

断点调试检查了LongSparseArray中的key数据,确实没有对应的值

调试的时候注意到这样一个问题

key = 8589934596

而LongSparseArray中的数据key为

4294967922
4294967923

程序员应当对这数字比较敏感

8589934596 = 0x200000004

4294967922 = 0x100000272

从上方key的计算逻辑中推导,可以看出是assertCookie不同

看起来我反射出的sPreloadedDrawables中并不一定包含我想要查找的资源


翻看Resource.loadDrawable的源码,发现drawabel也可能是从mDrawableCache中获取的

相关代码:

if (!mPreloading) {
    final Drawable cachedDrawable = caches.getInstance(key, theme);
    if (cachedDrawable != null) {
        return cachedDrawable;
    }
}

这个DrawableCache类本身只有包访问权限,反射代码还要写一堆,好在Debug模式下可以直接在resource里看到这个对象

Demo应用中drawable文件夹下只有2个资源,一张是我塞进去的测试图片,一张的ic_launch

检查了一下其持有的keys后,果然找到了8589934596.

于是下一步可以反射mDrawableCache并修改其中数据.

注意一个问题.这个 android.content.res.DrawableCache 类,只有包访问权限

不能使用Class.forName(“android.content.res.DrawableCache”)加载


这里我犯了个错误,我调试时使用的genymotion模拟器是5.0.1的

在API21版本中 drawableCache的实现是不同的

API21

private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> mDrawableCache =
         new ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>>();

API23

private final DrawableCache mDrawableCache = new DrawableCache(this);

因为这个原因,在反射对象上浪费了一些时间,以后应当注意这个问题.
研究源码相关的东西时,一定要使用相同版本的设备/模拟器,不然完全是浪费时间.


换成6.0设备测试了一下,成功拿到了我想要的Drawable对象

代码如下

Resources resource = getApplicationContext().getResources();
 Object mdrawableCache = null;
 Field field = null;
 try {
     field = Resources.class.getDeclaredField("mDrawableCache");
 } catch (NoSuchFieldException e) {
     e.printStackTrace();
 }
 field.setAccessible(true);
 try {
     mdrawableCache = field.get(resource);
 } catch (IllegalAccessException e) {
     e.printStackTrace();
 }

 TypedValue value = new TypedValue();
 resource.getValue(R.drawable.charming,value,true );

 long  key = -1;
 if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
         && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
     key = value.data;
 } else {
     key = (((long) value.assetCookie) << 32) | value.data;
 }

 Method method = null;
 try {
     Class  c = mdrawableCache.getClass();
     method = c.getDeclaredMethod("getInstance",long.class,Resources.Theme.class);
 } catch (NoSuchMethodException e) {
     e.printStackTrace();
 }
 Drawable drawable = null;
 try {
     drawable = (Drawable) method.invoke(mdrawableCache, key, null);
 } catch (IllegalAccessException e) {
     e.printStackTrace();
 } catch (InvocationTargetException e) {
     e.printStackTrace();
 }

下面考虑替换该Drawable并刷新View,参考ThemedResourceCache源码,猜测可以调用put方法把修改后的Drawable对象塞进去.

由于ThemeResourceCache持有的实际上还是Drawable.ConstantState对象,Drawable对象由其newDrawable()方法获取,所以应当构建BitmapState对象

这里依然非常蛋疼,BitmapState是BitmapDrawable的静态内部类,default,只有包访问权限.

无论是构造对象,调用方法,修改参数,都需要通过反射,感觉真的是非常非常麻烦.


从研究过程中看,行为依赖Resource本身DrawableCache和Preload的实现,而且5.0和6.0其实现逻辑又不同.

通过反射hack cache来做资源替换看起来并不是一个稳妥的方案.

Bubble-Notification Update

Bubble-Notification Update

Bubble-Notification

https://github.com/dkmeteor/Bubble-Notification

做了一次大更新

记录一下相关内容

#1.Bug fix

1.1 贝塞尔曲线调整

看了这篇文章

QQ手机版 5.0“一键下班”设计小结以后,修改了一下贝塞尔曲线的绘制方式

这文章作者是个设计师,并不是程序员

我也没有完全按他的去画这个贝塞尔曲线,优化了一下曲线函数,主要贝塞尔曲线参考点的修改,修复几个参考点计算错误的问题

1.2 修复由StatusBar高度导致的位置计算偏移

检讨一下,由于这部分公式计算比较复杂,且两种绘图方式坐标不统一,代码中有大量的位置换算,导致了很多小的误差.

#2. 新功能

##2.1 添加特效

最近看了 ExplosionField , ParticleLayout等项目,有了一些新的想法,打算更新一下Bubble-Notification的特效,添加几个新的.

由于Bubble-Notification的Explosion本身设计上是相当于一个微型粒子引擎,通过添加新粒子类型,给粒子添加更多属性和字段,可以挺方便的定制各种特效.

不过凑巧,那天正好戳到一个AE教程,稍微看了一下,然后我意识到, AE作为专业的特效工具,其提供的功能远远不是目前这种简陋的效果能比的.

结合起来也许能做一些更有趣的东西.

##2.2 添加GifRender

AE导出的效果只能是movie gif 或 序列帧,目前看来使用比较方便的只有gif和序列帧
先添加一个Gif支持来试试水

由于所有效果都直接绘制在SurfaceView上,需要自身控制,所以不方便使用第三方Gif库,好在通过android.graphics.Movie自己撸一个也很简单.

写了GifRender包装了一下,为了和Explosion保持一致,添加了GifUpdateThread用来刷新Gif

#3. 重构

添加GifRender过程后,由于以后可能还会添加新的特效模式,于是重构了下代码,将最后Explosion特效相关的代码从DropCover类中全部抽离,DropCover只负责贝塞尔曲线和圆形的绘制.

相关杂七杂八过程控制代码都丢入CoverMananger类中,这不是一个特别好的方案,不过把200行代码按OOP的方针分割成4~5个类也不是特别好的方案.暂且保持这样.如果控制过程的代码在将来会膨胀的话,到时候必须将CoverMananger中的代码拆分成各个独立的功能模块,不过目前没有必要.


具体算法过程描述:

这个一定要写一下,看半年前的代码完全看不懂了,到处都是坑,90%的问题都是 坐标点换算造成的.

    private void drawDrop() {
    Canvas canvas = getHolder().lockCanvas();
    if (canvas != null) {
        canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);

        if (isDraw) {
            double distance = Math.sqrt(Math.pow(mBaseX - mTargetX, 2) + Math.pow(mBaseY - mTargetY, 2));
            mPaint.setColor(0xffff0000);
            mPaint.setStyle(Paint.Style.FILL);
            if (distance < mMaxDistance) {
                //计算圆形半径
                mStrokeWidth = (float) ((1f - distance / mMaxDistance) * mRadius);
                //绘制 原位置的圆形,会随着拖动距离还减小,注意mBaseX,mBaseY是圆心坐标
                canvas.drawCircle(mBaseX, mBaseY, mStrokeWidth / 2, mPaint);
                drawBezier(canvas);
            }
            //绘制拖动后的图形,注意mTargetX,mTargetY是左上角坐标
            canvas.drawBitmap(mDest, mTargetX, mTargetY, mPaint);
        }
        getHolder().unlockCanvasAndPost(canvas);
    }
}

注意drawCircledrawBitmap 使用的坐标是不一样的,一个是圆心,一个是图片左上角.
进行坐标计算时使用的都是 圆心坐标,可以参考下面的图片

注意换算,非常操蛋.


算法详细

可以参考QQ手机版 5.0“一键下班”设计小结,基本上是一致的,就是贝塞尔曲线的参考点不会移动

计算2根贝塞尔曲线的4个起点/终点

看代码可能不明白,还原下其实就是简单的三角函数

/**
 * ax=by=0 x^2+y^2=s/2
 * <p/>
 * ==>
 * <p/>
 * x=a^2/(a^2+b^2)*s/2
 *
 * @param start
 * @param end
 * @return
 */
private Point[] calculate(Point start, Point end) {
    float a = end.x - start.x;
    float b = end.y - start.y;

    float y1 = (float) Math.sqrt(a * a / (a * a + b * b) * (mStrokeWidth / 2f) * (mStrokeWidth / 2f));
    float x1 = -b / a * y1;

    float y2 = (float) Math.sqrt(a * a / (a * a + b * b) * (targetWidth / 2f) * (targetHeight / 2f));
    float x2 = -b / a * y2;


    Point[] result = new Point[4];

    result[0] = new Point(start.x + x1, start.y + y1);
    result[1] = new Point(end.x + x2, end.y + y2);

    result[2] = new Point(start.x - x1, start.y - y1);
    result[3] = new Point(end.x - x2, end.y - y2);

    return result;
}

绘制贝塞尔曲线

贝塞尔曲线使用的参考点为 centerX,centerY,为两圆圆心连线的中点

private void drawBezier(Canvas canvas) {

    Point[] points = calculate(new Point(mBaseX, mBaseY), new Point(mTargetX + mDest.getWidth() / 2f, mTargetY + mDest.getHeight() / 2f));

    float centerX = (points[0].x + points[1].x + points[2].x + points[3].x) / 4f;
    float centerY = (points[0].y + points[1].y + points[2].y + points[3].y) / 4f;

    Path path1 = new Path();
    path1.moveTo(points[0].x, points[0].y);
    path1.quadTo(centerX, centerY, points[1].x, points[1].y);
    path1.lineTo(points[3].x, points[3].y);

    path1.quadTo(centerX, centerY, points[2].x, points[2].y);
    path1.lineTo(points[0].x, points[0].y);
    canvas.drawPath(path1, mPaint);
}

按腾讯那文章来看,参考点随距离移动效果可能会更好,我尝试了一下,看不出明显的区别.


加了个Note:

The origin circle drawn by  canvas.drawCircle(mBaseX, mBaseY, mStrokeWidth / 2, mPaint);

so mBaseX, mBaseY are center position.

The Moved circle drawn by  canvas.drawBitmap(mDest, mTargetX, mTargetY, mPaint);

so mTargetX,mTargetY are left top corner position.

计算过程中时刻记得这2点,之前好多bug就是因为这个造成的.


Explosion

其实这个东西非常简单.

一个简单的粒子生成器,实例化直接生成所有粒子

随机生成每个例子的颜色 x速度 y速度

然后由ExplosionUpdateThread刷新每个粒子的坐标并绘图

正常的 粒子引擎 功能要复杂的多的多, 不过上cocos2d或者别的图形/游戏引擎感觉没有必要

PathEffectTextView Update

更新了一下PathEffectTextView

  • 添加了字体大小 颜色 字重(bold) 阴影 等设置项.

  • 更新了下Demo.

  • 做了几个新的Demo Gif

  • 发布到Jcenter

TODO:

  • 等有空的时候,添加各种笔刷.

#Screenshot

Please waiting for loading the gif…

#How to use

Step 1: add denpendence

compile('com.dk.view.patheffect:Library:0.1.1@aar')

If you are still using Eclipse, you can just copy source code or jar file to you project.

Step 2: add view to your layout:

<com.dk.view.patheffect.PathTextView
    android:id="@+id/path"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

step 3: call init method like this:

PathTextView mPathTextView = (PathTextView) findViewById(R.id.path);
mPathTextView.init("Hello World");

Option settings:

mPathTextView.setPaintType(PathTextView.Type.MULTIPLY);
mPathTextView.setTextColor(color);
mPathTextView.setTextSize(size);
mPathTextView.setTextWeight(weight);
mPathTextView.setDuration(duration);
mPathTextView.setShadow(radius, dx, dy, shadowColor);

#NOTE

SurfaceView flashes black on load

打算更新一下Bubble-Notification.

之前所有效果都是通过在WindowMananger上添加DropCover,然后绘制在屏幕最顶层,类似悬浮窗的处理方案,为此应用需要拥有SYSTEM_ALERT权限(这里其实我的误解,使用TYPE_APPLICATION就不需要了).

后来想到参考SwipeBack/ResideMenu之类类库的,可以在DecorView上操作这些东西,这样就不需要权限了.

修改本身蛮简单的,不过修改完后发现,在效果最开始,屏幕会闪烁一下.

断点调了一会,是在SurfaceView被添加进DropCover后发生的,此时SurfaceView上还未进行任何绘制.

奇怪的是第二次再添加SurfaceView时,又不会闪烁了,因为修改以前运行是正常的,排除了绘制的问题以后.

搜索了一下,发现了这个

http://www.mzule.com/%E9%81%BF%E5%85%8D-surfaceview-%E7%AC%AC%E4%B8%80%E6%AC%A1%E6%98%BE%E7%A4%BA%E5%AF%BC%E8%87%B4%E7%9A%84%E9%BB%91%E5%B1%8F/

http://stackoverflow.com/questions/8772862/surfaceview-flashes-black-on-load

防止链接失效摘录一下原文:

当 SurfaceView 第一次在 Window 里面显示的时候,会触发 IWindowSession.relayout(…) 方法,该方法会 relayout 整个布局,导致屏幕黑屏一下。解决方案,就是在 Activity 加入一个默认就显示的 SurfaceView,可以通过设置它的宽度为 0,来避免用户看见它。这样在 Fragment 里面的 SurfaceView 就已经是第二个 SurfaceView 了。可以重用上一个 SurfaceView 的参数,避免的明显的屏幕黑屏。

StackOverFlow上那个答案是这样:

I think I found the reason for the black flash. In my case I'm using a SurfaceView inside a Fragment and dynamically adding this fragment to the activity after some action. The moment when I add the fragment to the activity, the screen flashes black. I checked out grepcode for the SurfaceView source and here's what I found: when the surface view appears in the window the very fist time, it requests the window's parameters changing by calling a private IWindowSession.relayout(..) method. This method "gives" you a new frame, window, and window surface. I think the screen blinks right at that moment.

The solution is pretty simple: if your window already has appropriate parameters it will not refresh all the window's stuff and the screen will not blink. The simplest solution is to add a 0px height plain SurfaceView to the first layout of your activity. This will recreate the window before the activity is shown on the screen, and when you set your second layout it will just continue using the window with the current parameters. I hope this helps.

在MainActivity上添加了一个0px*0px的SurfaceView以后,问题确实解决了.


但是这个解决方案实在太难看了,产生问题的原因也很反常.

查看SurfaceView的源码,可以找到上面所说的调用位置

protected void updateWindow(boolean force, boolean redrawNeeded){
    ...
     relayoutResult = mSession.relayout(
                        mWindow, mWindow.mSeq, mLayout, mWidth, mHeight,
                            visible ? VISIBLE : GONE,
                            WindowManagerGlobal.RELAYOUT_DEFER_SURFACE_DESTROY,
                            mWinFrame, mOverscanInsets, mContentInsets,
                            mVisibleInsets, mStableInsets, mConfiguration, mNewSurface);
    ...
} 

尝试了若干办法,绕过或者提前通过getWindow()获取Window对象设置参数都不行.

由于我是提供的类库,如果需要使用者自己去加个SurfaceView那也太操蛋了,这样Hack了一下

public void init(Activity activity) {
if (mDropCover == null) {
    mDropCover = new DropCover(activity);
    mContainer = new FrameLayout(activity);

    ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
    decor.addView(mContainer);

    /**
     *
     *  WTF!
     *
     *  http://stackoverflow.com/questions/8772862/surfaceview-flashes-black-on-load
     *
     */
    SurfaceView s = new SurfaceView(activity);
    mContainer.addView(s);
    mContainer.post(new Runnable() {
        @Override
        public void run() {
            mContainer.removeAllViews();
        }
    });
}

StackOverFlow上那个提问记录还是2012年的…


又仔细思索了一下,这种处理方式还是很操蛋,把这一部分重新改回了使用WindowMananger添加的方式

这里是我的理解有误

使用TYPE_APPLICATION就可以了,不需要权限.

之前使用的是TYPE_SYSTEM_ALERT,所以才需要权限,修改绕了一圈又回来了.

Android 6.0 刷机各种坑记录(防砖)

从preview3刷成了正式版6.0

几个坑有点蛋疼,记录下,免得下次刷成砖.

  • 下载下来的hammerhead-mra58k-factory-52364034.tar文件,解压缩后会获得一个无后缀的文件,修改后缀为zip后,再解压一次,才能看到flash脚本

  • 直接执行flash-all. 刷preview3的时候,是直接flash-all就好了,这次刷正式版的时候,执行flash脚本以后一直提示system.img not found,检查了一下压缩包,system.img在里面,压缩包也没损坏,反复试了几次都是这个错误
    不能直接一次全刷上,好在可以把image-hammerhead-mra58k.zip压缩包解压了,然后手动一个一个刷img

fastboot flash bootloader bootloader-hammerhead-hhz12k.img
fastboot flash radio radio-hammerhead-m8974a-2.0.50.2.27.img

fastboot reboot-bootloader

fastboot flash recovery recovery.img
fastboot flash boot boot.img
fastboot flash system system.img
  • 刷完以后重启系统,正常开机,开始 应用优化 ,速度很慢.不过刷机以后能保留数据和应用不用去重装也是不错.然而这是个错误.进入系统以后发现几乎所有应用闪退.用root explorer的时候发现emulated文件夹下什么都看不见,才意识到可能是新的权限系统的问题.在设置-应用里翻了一下,果然所有非系统应用权限都是无.尝试修改应用权限失败,修改后重启没有变化.然后因为各种全家桶应用在不停的重启-崩溃-重启,整个手机非常卡.
  • 老老实实重启fastboot 然后
fastboot flash cache cache.img
fastboot flash userdata userdata.img
  • 重启系统后,正常开机,依然很慢,然后就是开机引导和蛋疼的联网验证.这里有个很蛋疼的问题,这一步其实是要绑定google账户,因为那啥啥的原因,你肯定是联不上的,但是你手机插着sim卡,就会自动开启3G,当然你还是联不上,然后这一步也无法跳过.虽然有个跳过按钮,但是放了半个小时还是灰的.
  • 正确的方法是,连接wifi(必须),高级里面proxy放一个https代理.这里应该是必须要https,反正我用http代理没成功过,不过免费代理本来就不稳定,也不确定.
  • 然后等上一会,就发现可以跳过了,或者直接绑上也可以,我刷preview的时候是直接绑好了,这次可以跳过我就跳过了.

  • 另外个方案是 不联网,把sim卡拔出来,应该也是能跳过这个验证步骤的,不过我取卡针不知道扔哪去了,卡拿不出来..

  • 目前看起来还没发现什么问题,和preview3看起来也没什么区别.

Vysor

最近大家都在说vysor,于是我也弄来体验了一下.

只能说达不到预期的效果吧.

几个问题:

  • 帧率低
  • 动画的时候画面会变模糊
  • 手机熄屏后,vysor也黑了

对于开发来讲,意义不是特别大.

你可以用GenyMotion或者Android Studio自带模拟器,体验基本上是一样的.

而必须用到真机测试的东西,往往你没法在vysor上完成,比如拍照,摇一摇之类的,测试的时候还是要把手机拿起来折腾

img

一般情况下还好,左右滑动的时候,就会迅速变得非常模糊,一会之后才会恢复.

不过如果电脑内存比较小,开了模拟器会爆内存的话,这个还是挺有用的,可以让你Build以后手不离开键盘鼠标就完成测试操作.减少打断和干扰吧.

Update Radial Blur Library

因为看了@drakeet的FingerTransparentView想起来去年自己也写过一个类似的动态模糊(Motion Blur)的库

于是翻出来看了一下,花了点时间迁移到了Android Studio

在看的时候自己发现一个问题,这TM根本不是 Motion Blur ,我自己写的代码实际上实现的效果是 径向模糊(Radial Blur)

嘛…我还是要说一下 MotionBlur和RadialBlur看起来很像,但实际上是不一样的,我之前也是误解了,后来仔细看了些效果图,下面提到的几个库算法实现虽然名字都是叫MotionBlur,但是实现的效果其实都是RadialBlur

Demo效果

Examples list

因为涉及到重复draw,当时我写得时候就记得性能很一般,这次迁移顺便也打算看看这个问题

核心代码其实很简单

  public static Bitmap doRadialBlur(Bitmap src, int centerX, int centerY, float factor, int times) {

    Bitmap dst = Bitmap.createBitmap(src.getWidth(), src.getHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(dst);
    Matrix matrix = new Matrix();
    Paint paint = new Paint();
    canvas.drawBitmap(src, matrix, paint);
    paint.setAlpha(51);

    for (int i = 0; i < times; i++) {
        matrix.setScale(i * factor + 1, i * factor + 1, centerX, centerY);
        canvas.drawBitmap(src, matrix, paint);
    }
    return dst;
}

为了实现效果需要反复的Scale并重复的draw同一张Bitmap,性能当然狠捉急.
测试时发现渲染一次需要 1400ms 左右.

图片是24002400的,换成800800的以后,减少到200ms左右,作为一个滤镜来讲,尚可接受吧,一时间也想不到什么优化的方法.
优化到16ms以下可以做成实时滤镜,否则的话,似乎意义不大.

想起jhlab也有 径向模糊 的滤镜算法,于是拔出来代码看了下

 public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
    if ( dst == null )
        dst = createCompatibleDestImage( src, null );
    BufferedImage tsrc = src;
    float cx = (float)src.getWidth() * centreX;
    float cy = (float)src.getHeight() * centreY;
    float imageRadius = (float)Math.sqrt( cx*cx + cy*cy );
    float translateX = (float)(distance * Math.cos( angle ));
    float translateY = (float)(distance * -Math.sin( angle ));
    float scale = zoom;
    float rotate = rotation;
    float maxDistance = distance + Math.abs(rotation*imageRadius) + zoom*imageRadius;
    int steps = log2((int)maxDistance);

    translateX /= maxDistance;
    translateY /= maxDistance;
    scale /= maxDistance;
    rotate /= maxDistance;

    if ( steps == 0 ) {
        Graphics2D g = dst.createGraphics();
        g.drawRenderedImage( src, null );
        g.dispose();
        return dst;
    }

    BufferedImage tmp = createCompatibleDestImage( src, null );
    for ( int i = 0; i < steps; i++ ) {
        Graphics2D g = tmp.createGraphics();
        g.drawImage( tsrc, null, null );
        g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
        g.setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR );
        g.setComposite( AlphaComposite.getInstance( AlphaComposite.SRC_OVER, 0.5f ) );

        g.translate( cx+translateX, cy+translateY );
        g.scale( 1.0001+scale, 1.0001+scale );  // The .0001 works round a bug on Windows where drawImage throws an ArrayIndexOutofBoundException
        if ( rotation != 0 )
            g.rotate( rotate );
        g.translate( -cx, -cy );

        g.drawImage( dst, null, null );
        g.dispose();
        BufferedImage ti = dst;
        dst = tmp;
        tmp = ti;
        tsrc = dst;

        translateX *= 2;
        translateY *= 2;
        scale *= 2;
        rotate *= 2;
    }
    return dst;
}

Jhlabs比较古老,BufferedImage是awt里面,做Android的如果不了解的话当Bitmap理解就好了.
可以看到Jhlabs的代码逻辑也没什么区别,一样是通过反复draw image实现的径向模糊效果.

参考意义不大.

另外在Github上搜索了一下,一个叫做AndroidFastImageProcessing的库也支持MotionBlur,其实现机制比较特别

核心代码是这样

protected String getFragmentShader() {
    return 
             "precision mediump float;\n" 
            +"uniform sampler2D "+UNIFORM_TEXTURE0+";\n"  
            +"varying vec2 "+VARYING_TEXCOORD+";\n"    
            +"uniform float "+UNIFORM_TEXELWIDTH+";\n"
            +"uniform float "+UNIFORM_TEXELHEIGHT+";\n"

              +"void main(){\n"
              +"   vec2 step = vec2("+UNIFORM_TEXELWIDTH+", "+UNIFORM_TEXELHEIGHT+");\n"
              +"   vec4 fragColour = texture2D("+UNIFORM_TEXTURE0+", "+VARYING_TEXCOORD+") * 0.18;\n"
              +"   fragColour += texture2D("+UNIFORM_TEXTURE0+", "+VARYING_TEXCOORD+" + step) * 0.15;\n"
              +"   fragColour += texture2D("+UNIFORM_TEXTURE0+", "+VARYING_TEXCOORD+" - step) * 0.15;\n"
              +"   fragColour += texture2D("+UNIFORM_TEXTURE0+", "+VARYING_TEXCOORD+" + step * 2.0) * 0.12;\n"
              +"   fragColour += texture2D("+UNIFORM_TEXTURE0+", "+VARYING_TEXCOORD+" - step * 2.0) * 0.12;\n"
              +"   fragColour += texture2D("+UNIFORM_TEXTURE0+", "+VARYING_TEXCOORD+" + step * 3.0) * 0.09;\n"
              +"   fragColour += texture2D("+UNIFORM_TEXTURE0+", "+VARYING_TEXCOORD+" - step * 3.0) * 0.09;\n"
              +"   fragColour += texture2D("+UNIFORM_TEXTURE0+", "+VARYING_TEXCOORD+" + step * 4.0) * 0.05;\n"
              +"   fragColour += texture2D("+UNIFORM_TEXTURE0+", "+VARYING_TEXCOORD+" - step * 4.0) * 0.05;\n"
              +"   gl_FragColor = fragColour;\n"
              +"}\n";
}

可以看到 这是生成了一段代码

然后对应的执行部分

GLES20.glShaderSource(fragmentShaderHandle, fragmentShader);
GLES20.glCompileShader(fragmentShaderHandle);

好吧,其实我压根就不懂OpenGl.大概是把图片作为Shader进行了多次渲染来实现MontionBlur的效果.

这个研究基本上没有结果,往下层去看都是native方法,OpenGl可能是一个可行的优化方法,可惜是我不懂OpenGL,看了一点点实在没什么头绪,可能需要系统的从头学习一下.


然后过程中还发现个比较奇特的问题

将项目发布到jcenter以后,打算自己测试一下,但是一直编译不过,提示如下:

Error:A problem occurred configuring project ':app'.
> Could not resolve all dependencies for configuration ':app:_debugCompile'.
   > Could not resolve com.dk.image.process.radialblur:library:0.1.0.
     Required by:
         MotionBlur-Android:app:unspecified
      > Could not resolve com.dk.image.process.radialblur:library:0.1.0.
         > Could not get resource 'https://jcenter.bintray.com/com/dk/image/process/radialblur/library/0.1.0/library-0.1.0.pom'.
            > Could not HEAD 'https://jcenter.bintray.com/com/dk/image/process/radialblur/library/0.1.0/library-0.1.0.pom'. Received status code 400 from server: Bad Request

我自己反复确认了几次, https://jcenter.bintray.com/com/dk/image/process/radialblur/library/0.1.0/library-0.1.0.pom 绝对是可以访问的.

然后我一想,可能是代理的问题,但是我确定自己肯定没给Android Studio配过代理,因为我自己用的代理是 红杏,需要全局代理的时候我用的mxvpm需要客户端登陆,根本不会在Android Studio里配置,
不过还是检查了下Android Studio的代理配置,代理配置确实是空的,但是最顶部有一个Warnning提示,提示我JVM里配置了一个Proxy

mirrors.opencas.cn

我其实不大理解什么叫JVM PROXY,不过这个代理服务器地址我一看就知道是我在SDK Manager里使用的镜像的地址,我也不知道为什么Android Studio会把SDK Manager里的配置读过来,之前是从来没有过的.
打开SDK Manager,删掉镜像配置,重启Android Studio,恢复了正常,denpendices可以正常sync.

关掉Android Studio,再打开SDK Manager,把镜像重新配回去.重启Android Studio,检查代理设置,正常.

这我就不理解了,什么情况下Android Studio会读取SDK Mananger的代理配置,什么时候不会?

出现问题的版本 Android Studio版本1.3.1

之前从来没有遇到过,因为我SDK Manager里一直是挂着镜像的地址.

关于Looper的瞎扯蛋

#关于Looper的瞎扯蛋

知乎有这么一个问题

Android中为什么主线程不会因为Looper.loop()里的死循环卡死?


这问题本身其实很简单,对Android有一定了解的都会学习Handler Looper MessageQueue相关的知识,面试的时候我也喜欢问别人这个问题.

for (;;) {
    Message msg = queue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return;
    }

    // This must be in a local variable, in case a UI event sets the logger
    Printer logging = me.mLogging;
    if (logging != null) {
        logging.println(">>>>> Dispatching to " + msg.target + " " +
                msg.callback + ": " + msg.what);
    }

    msg.target.dispatchMessage(msg);

    if (logging != null) {
        logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
    }

    // Make sure that during the course of dispatching the
    // identity of the thread wasn't corrupted.
    final long newIdent = Binder.clearCallingIdentity();
    if (ident != newIdent) {
        Log.wtf(TAG, "Thread identity changed from 0x"
                + Long.toHexString(ident) + " to 0x"
                + Long.toHexString(newIdent) + " while dispatching to "
                + msg.target.getClass().getName() + " "
                + msg.callback + " what=" + msg.what);
    }

    msg.recycleUnchecked();
}

要我简单点回答的话,我觉得答案就是 阻塞队列 BlockingQueue

这里比较有意思的这个问题里的有个些答案让我很意外

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
作者:TracyB
链接:http://www.zhihu.com/question/34652589/answer/59722754
来源:知乎


epoll+pipe,有消息就依次执行,没消息就block住,让出CPU,等有消息了,epoll会往pipe中写一个字符,把主线程从block状态唤起,主线程就继续依次执行消息,怎么会死循环呢…

这个逻辑我猜大概是这样

为什么不会死循环 –> 这里使用了阻塞队列 –> 阻塞队列底层调度如何实现 –> epoll

我猜这人大概是做Framework层 或 ROM开发的, 一般Android程序员恐怕不会从这个角度去想问题.


ps.顺便一提

Looper中通过 ThreadLocal 来为每个线程分配隔离的 Looper对象

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

关于ThreadLocal,以前都是当做Java特性的来理解这个东西的,无聊看了下源代码

实现机制其实非常简洁,我觉得我以前看过的书上 对 ThreadLocal的介绍&分析真是浪费时间, 核心代码也没几行, 看源码5分钟就能理解的东西,书上啰啰嗦嗦的讲上N页

public T get() {
    // Optimized for the fast path.
    Thread currentThread = Thread.currentThread();
    Values values = values(currentThread);
    if (values != null) {
        Object[] table = values.table;
        int index = hash & values.mask;
        if (this.reference == table[index]) {
            return (T) table[index + 1];
        }
    } else {
        values = initializeValues(currentThread);
    }

    return (T) values.getAfterMiss(this);
}

public void set(T value) {
    Thread currentThread = Thread.currentThread();
    Values values = values(currentThread);
    if (values == null) {
        values = initializeValues(currentThread);
    }
    values.put(this, value);
}

Values values(Thread current) {
    return current.localValues;
}

几个关键方法源代码一看,每个Thread会持有一个localValues对象,set的时候,对象被直接塞给了localValues
localValues是一个类似HashMap的数据结构,ThreadLocal对象自身做key

简单明了,看代码根本不会产生任何理解上的误差,以前像记概念一样看这个东西真是犯傻.


ps2.顺便我就想到了volatile

这个玩意也是一直是当做概念去记的. 比如JAVA核心技术上用了一大段晦涩的文字来描述这是个什么玩意,印象中还在其它书上看到更复杂的解释.

实际上 一旦更深入的划分 内存 这个概念 , 这个东西就很好理解了,而且不会再记错用错.

CPU寄存器中的cache是内存, JVM的 线程工作内存 也是内存.