Folder-ResideMenu性能优化(Performance update for Folder-ResideMenu)

#Folder-ResideMenu性能优化(Performance update for Folder-ResideMenu)

Folder-ResideMenu的核心部分算完成了,剩下的就是修复一些bug,于是我顺便做了下性能测试
结果如下:

image

WTF??!!

目测单帧 50ms左右.

好吧,于是我给自己开了个issue并且着手修复这个问题.

https://github.com/dkmeteor/Folder-ResideMenu/issues/3

恩,这一定是个bug..


  • 首先观察一下Logcat , 没有任何异常输出
  • 看一下Memory monitor , 额…内存抖动….

image

public float[] createOffsetVerts(float offset, float pointerY) {
    applyCurveXEffect(offset);
    applyScaleXEffect(offset, pointerY);
    mShader = applyShadow(offset);
    return meshVerts;
}

算法核心在这里,一开始设计的时候就将效果分解成若干步了,正好可以分开一个一个调试.

因为公式太复杂了,写在一起 代码->公式 脑子里映射不出来,改起来太吃力,否则在一个循环里把verts都算好性能更好,不过那样就完全没有可读性了,至少我自己是看不懂了…

分别把每个步骤注释了再运行一下试试.
(虽然看到内存抖动的时候就想到大概是BitmapShader的问题了)

可以看到,干掉Shader以后,性能获得了无与伦比的提示

image

目测提升了10倍吧…单帧在4ms左右…
Shader就是扭曲效果之上渲染的阴影,发个对比图比较直观

阴影的Alpha会随着滑动不断加深来模拟褶皱越来越深的效果

无Shader ___ 有Shader

image image

找到关键点以后事情就变得简单了

生成Shader的代码

private Shader applyShadow(float offset) {

    float p1 = (float) Math.sqrt(Math.PI / 2 * SIN_lENGTH_FACTOR);
    float p2 = (float) Math.sqrt((Math.PI / 2 + 1 * Math.PI) * SIN_lENGTH_FACTOR);
    float p3 = (float) Math.sqrt((Math.PI / 2 + 2 * Math.PI) * SIN_lENGTH_FACTOR);
    float p4 = (float) Math.sqrt((Math.PI / 2 + 3 * Math.PI) * SIN_lENGTH_FACTOR);
    float p5 = (float) Math.sqrt((Math.PI / 2 + 4 * Math.PI) * SIN_lENGTH_FACTOR);
    float p6 = (float) Math.sqrt((Math.PI / 2 + 5 * Math.PI) * SIN_lENGTH_FACTOR);
    float p7 = (float) Math.sqrt((Math.PI / 2 + 6 * Math.PI) * SIN_lENGTH_FACTOR);
    float p8 = (float) Math.sqrt((Math.PI / 2 + 7 * Math.PI) * SIN_lENGTH_FACTOR);
    float p9 = (float) Math.sqrt((Math.PI / 2 + 8 * Math.PI) * SIN_lENGTH_FACTOR);

    int gray = ((int) (mAlpha * ((1l - offset) * 1f)) << 24)
            | GRAY;
    Shader shader = null;

    if (mDirection == TouchDisableView.DIRECTION_RIGHT) {
        shader = new LinearGradient(0, 0, width, 0, new int[]{gray,
                TRANSPARENT, gray, TRANSPARENT, gray, TRANSPARENT, gray,
                TRANSPARENT, gray}, new float[]{p1 / width, p2 / width,
                p3 / width, p4 / width, p5 / width, p6 / width, p7 / width,
                p8 / width, p9 / width}, Shader.TileMode.REPEAT);
    } else {
        shader = new LinearGradient(0, 0, width, 0, new int[]{gray,
                TRANSPARENT, gray, TRANSPARENT, gray, TRANSPARENT, gray,
                TRANSPARENT, gray}, new float[]{(1 - p9 / width), (1 - p8 / width),
                (1 - p7 / width), (1 - p6 / width), (1 - p5 / width), (1 - p4 / width), (1 - p3 / width),
                (1 - p2 / width), (1 - p1 / width)}, Shader.TileMode.REPEAT);
    }
    return shader;
}

看起来有些小问题,每次回调的时候会根据offset值重新计算 Shader的颜色(Alpha)
每次setFolder回调都会new出一个新的 LinearGradient 对象

上面一大堆常量可以扔出去,不用每次都计算

但是都不像是会引起内存抖动的问题. ‘LinearGradient’这个对象并不大,即使每帧都new一个,也不会产生肉眼可见的内存抖动…

不过也可以优化一下,显然,更新Shader只是为了调整阴影的透明度,这工作扔到paint上做就好了

修改为每次 setDirection 时,将mShader设置为null

然后

if (mShader == null)
    mShader = applyShadow(offset);

将ShaderAlpha移到MeshImageView中.由Paint去处理Shadow层的alpha问题.

再顺着Shader找找Shader处理阴影层的地方

@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    if (mPaint == null)
        mPaint = new Paint();

    if (mShader != null) {
        mShaderBitmap = Bitmap.createBitmap(canvas.getWidth(),
                canvas.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas tempCanvas = new Canvas(mShaderBitmap);
        Paint paint = new Paint();
        paint.setShader(mShader);
        paint.setAlpha((int)(mShaderAlpha * 255f));
        tempCanvas.drawPaint(paint);
    }
    if (mVerts != null) {
        canvas.drawBitmapMesh(mBitmap, 50, 5, mVerts, 0, null, 0, null);
        if (mShaderBitmap != null)
            canvas.drawBitmapMesh(mShaderBitmap, 50, 5, mVerts, 0, null, 0,
                    null);
    } else
        canvas.drawBitmap(mBitmap, new Matrix(), mPaint);
}

Shit , 竟然在draw里去生成Bitmap……看来内存抖动就是它了….
仔细回忆了一下…我也想不起来当初为什么这么写了…可能只是为了更新阴影状态??
(无论任何时候,你看你三个月前写的代码,都觉得是一坨狗屎)
修改后如下

@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
    if (mPaint == null)
        mPaint = new Paint();

    if (mShader != null && mShaderBitmap == null) {
        mShaderBitmap = Bitmap.createBitmap(canvas.getWidth(),
                canvas.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas tempCanvas = new Canvas(mShaderBitmap);
        Paint paint = new Paint();
        paint.setShader(mShader);
        tempCanvas.drawPaint(paint);
    }
    if (mVerts != null) {
        canvas.drawBitmapMesh(mBitmap, 50, 5, mVerts, 0, null, 0, null);
        if (mShaderBitmap != null) {
            mAlphaPaint.setAlpha((int) (mShaderAlpha * 255f));
            canvas.drawBitmapMesh(mShaderBitmap, 50, 5, mVerts, 0, null, 0,
                    mAlphaPaint);
        }
    } else
        canvas.drawBitmap(mBitmap, new Matrix(), mPaint);
}

tempCanvas只会创建一次,看来仍在这里也没什么问题

再跑起来看一下

image

哦哦 看起来非常不错了…..

对比一下QQ

image

当然QQ整个View比我这个Demo复杂的多,光View层级就得复杂个几十倍把,
不过这锯齿形的帧率也很奇怪..

但是我注意到….我的有2帧绘制的非常慢?
What is that?

反复操作了几次以后发现,那个长条就是缩放动画开始的第一帧…
第一帧之前做了什么? 截取当前View生成Bitmap
把当前ContentView偷偷换成了Bitmap截图
当然,根据优化后的代码,还生成了一个ShaderBitmap

ok

下一步来研究这个问题…


首先可以想到,最容易造成性能问题的,自然就是 Bitmap生成了
还记得上面做的Shader优化的话
我们会在 动画触发 的时候

生成一个 BitmapShader

简单的添加了个log

05-28 16:23:03.908 4153-4153/com.dk.sample.folder.residemenu I/System.out﹕ CreateShader:34

反复运行了几次以后,发现生成Shader大约在25~35ms左右
这样的话 这一帧渲染时间当然一定会超过16ms了…

仔细思考一下这个系统,我其实只需要2个方向的不同Shader
也许预先把他们生成好并cache起来,就不必在ondraw时再去生成了
反正Shader和content内容本身没什么关系…

从源码分析 对 非UI线程不允许访问UI元素的 理解

在eoe回答了一个问题:
http://www.eoeandroid.com/forum.php?mod=viewthread&tid=579938&page=1

索性把内容整理一下…


做Android开发初期大部分开发者都会遇到下面这个问题.

android.view.viewroot$calledfromwrongthreadexception: only the original thread that created a view hierarchy can touch its views.

原因大半都是 由于在异步线程中视图 更新界面中的元素

百度一搜,可以搜到大量解决办法.

结果提炼一下: 大致就是讲 UI元素/视图元素 必须在 UI线程(主线程)中更新修改.

新手这样理解就够了.


但是仔细看这个Exception说明

only the original thread that created a view hierarchy can touch its views.

里面并没有提到 主线程/UI 线程,只是提到,必须是创建View hierarchy的线程

!注意区分

创建View的线程和创建View hierarchy的不一定是同一个线程,不要理解错了,下面在源码中具体分析

观察ViewRoot源码中抛出calledfromwrongthreadexception的位置

void checkThread() {

    if (mThread != Thread.currentThread()) {

        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

可以注意到 这里只检查了 当前执行线程和 mThread是否为同一个线程

public ViewRoot(Context context) {

     super();
     if (MEASURE_LATENCY &amp;&amp; lt == null) {
         lt = new LatencyTimer(100, 1000);
     }

     // For debug only
     //++sInstanceCount;
     // Initialize the statics when this class is first instantiated. This is
     // done here instead of in the static block because Zygote does not
     // allow the spawning of threads.

     getWindowSession(context.getMainLooper());

     mThread = Thread.currentThread();

由ViewRoot的构造函数可以看到,mThread被绑定在了 创建viewRoot的线程上.

当然大部分情况下,ViewRoot都是在主线程中创建的,所以在异步线程中修改view会造成checkThread失败.

mTextView.setText(“123”);为例子.

跟踪一下调用栈

-- android.widget.TextView.setText

  -- android.widget.TextView.checkForRelayout

    -- android.view.View.invalidate

        -- android.view.ViewGroup.invalidateChild

          -- android.view.ViewRoot.invalidateChildInParent

            -- android.view.ViewRoot.invalidateChild

              -- android.view.ViewRoot.checkThread

可以看到 view会不断遍历去获取 ViewParent
一个视图上的View遍历到最后 就是 获取到了 ViewRoot

(ViewRoot本身就是ViewParent的一个子类)

然后就会调用到上面提到的 checkThread()

到这里有一个结论:

UI修改操作确实会验证操作的线程

* * *

回到标题上来说, 因为 ViewRoot是由UI线程创建的,所以所有被添加到ViewRoot下的子View都必须在UI线程中修改,看起来也很合理.
那么,有可能在非UI线程中创建ViewRoot吗?

其实是可以的.

这里就涉及到ViewRoot和WindowManager之间的关系

demo代码如下:

package com.dk.asyncui;

import android.app.Activity;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.os.Looper;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;

public class MainActivity extends Activity {
    private Button mButton ;
    private WindowManager mWindowManager ;

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

           mWindowManager = (WindowManager) getSystemService("window" );

           mButton = new Button(this );
           mButton.setText( "HelloWorld");

          Thread mthread = new Thread(new Runnable() {

                  @Override
                  public void run() {
                       Looper. prepare();
                       WindowManager.LayoutParams wmParams = new WindowManager.LayoutParams();
                       wmParams. type = WindowManager.LayoutParams.TYPE_APPLICATION ;
                       wmParams. format = PixelFormat. RGBA_8888;
                       wmParams. flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL ;
                       wmParams. width = 320;
                       wmParams. height = 140;
                        mWindowManager.addView(mButton , wmParams);
                       Looper. loop();

                 }
          });
          mthread.start();
           mButton.setOnClickListener( new View.OnClickListener() {

                  @Override
                  public void onClick(View v) {

                       runOnUiThread( new Runnable() {
                               public void run() {
                                      mButton.setText( "22222222");
                              }
                       });
                 }
          });
   }

}

可以看到,我在异步线程中将 mButton添加到 WindowManager中

查看 WindowManangerImpl中addView的代码:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }

    if (display == null) {
        throw new IllegalArgumentException("display must not be null");
    }

    if (!(params instanceof WindowManager.LayoutParams)) {
        throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
    }
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    if (parentWindow != null) {
        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    }
    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
        // Start watching for system property changes.

        if (mSystemPropertyUpdater == null) {
            mSystemPropertyUpdater = new Runnable() {
                @Override public void run() {
                    synchronized (mLock) {
                        for (int i = mRoots.size() - 1; i &gt;= 0; --i) {
                            mRoots.get(i).loadSystemProperties();
                        }
                    }
                }
            };
            SystemProperties.addChangeCallback(mSystemPropertyUpdater);
        }
        int index = findViewLocked(view, false);
        if (index &gt;= 0) {
            if (mDyingViews.contains(view)) {
                // Don't wait for MSG_DIE to make it's way through root's queue.
                mRoots.get(index).doDie();
            } else {
                throw new IllegalStateException("View " + view
                        + " has already been added to the window manager.");
            }
            // The previous removeView() had not completed executing. Now it has.
        }
        // If this is a panel window, then find the window it is being
        // attached to for future reference.
        if (wparams.type &gt;= WindowManager.LayoutParams.FIRST_SUB_WINDOW &amp;&amp;
                wparams.type &lt;= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            final int count = mViews.size();
            for (int i = 0; i &lt; count; i++) {
                if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                    panelParentView = mViews.get(i);
                }
            }
        }
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }

    // do this last because it fires off messages to start doing things
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index &gt;= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

可以发现在addView的过程中 WindowMananger创建了 ViewRoot

这就是上面提到的在 异步线程中创建 ViewRoot

上面Demo代码中的 runOnUiThread是为了保证下面的
mButton.setText( “22222222” );

是在主线程中执行
可以看到,这段代码即使是在主线程中执行,依然抛出了CalledFromWrongThreadException

在特意构造的这个特殊场景下,主线程/UI线程中 不能更新在异步线程中”创建/添加”的UI元素!!


从源码和设计的角度上分析,我倾向于认为这其实是设计上的一个漏洞,我通过这个漏洞创建了一个非主线程绑定的ViewRoot对象,才会导致上面的结果.

PathEffectTextView

path3

path2

path1

https://github.com/dkmeteor/PathEffectTextView

90%完成吧…

昨天因为一个比较傻逼的原因看了半天为何Path画出来没效果….甚至以为PathMeasure的作用机制和我预想的不同,结果是因为Paint没在STROKE模式,简直醉了…..

与PathEffect结合使用可以做出更好的效果,不过目前并没有用到…..嘛…..反正标题就这样也懒得改了..以后无聊了再加吧..

与SVG解析器一起使用的话,理论上是任意图形都可以用这种方式做动效…

关于CheckBox的PaddingLeft

在不同Android版本中对PaddingLeft的计算方式不同

在Android 4.1.2 之前,Checkbox的 paddingleft 是从View最左侧开始计算的

而Android 4.1.2 之后,Checkbox的 paddingLeft 是从drawableLeft的右侧开始计算起的

2个版本之间的 paddingLeft会差一个 drawableLeft的宽度

一个兼容实现方式

//View hack for fucking difference between versions
        if (Build.VERSION.SDK_INT &lt;= 16) {//4.1.2
            mCheckBox.setPadding(ViewUtils.getPixels(20 + 10, mContext), 0, 0, 0);
            } else {
            mCheckBox.setPadding(ViewUtils.getPixels(10,mContext), 0, 0, 0);
        }

讨厌这种写法。

CircleAnimation Update

circle_animation

修改了实现方式,不再依赖RevealAnimator

也不再依赖

/** @hide */
public void setRevealClip(boolean shouldClip, float x, float y, float radius)</pre>

删除了反射的setRevealClip的部分


新的实现完全基于老的Canvas API,兼容低版本


缩放的部分加了个bounce效果,不过效果并不好,估计是系数选的不对,看来还是去找一套算好的系数。

最后再把相关接口调整一下,一坨AnimatorSet的嵌套逻辑最后用 playSequentially 整理一下应该就可以结束了。


顺带 看了下 CircularReveal https://github.com/ozodrukh/CircularReveal/ 的源代码

用这个方案来实现也可以,不过

先draw再clipPath

drawCircle with BitmapShader

从结果上来看是完全等价的。

以后需要在ViewGroup上实现该效果可能才用得上这个方案

CircleAnimation

circle_animation

下午和同事讨论转场效果,晚上蛋疼试了一下。

做起来还比较简单,不过RevealAnimator整个系列API 还全是 @Hide,最后反射了几个View的私有API hack了一下..

加速器曲线没仔细调,用sin函数凑合了一下

目前限制还比较多,依赖Container,仅支持API21, 因为反射的关系,也不用谈什么兼容性了…我自己都不知道那几个@hide 方法是哪个版本加的..grepcode不知道为毛挂着代理也访问不了,一下子还不知道去哪里查,@hide 的API官方文档上压根就找不到..

考虑兼容性要正常使用的话 动画的位置需要用 DecorView的根容器置换一下,然后这一套私有API要全部扔掉,换成ViewGroup里的canvas.clipPath(CirclePath)去实现

Euclid 源码分析

Euclid 源码分析

Euclid
https://github.com/Yalantis/Euclid

效果比较惊艳,clone下来分析一下源码~

99%的UI动效都是Trick…..
看完源码蛋都碎了….


看源码之前比较疑惑如何做到Activity切换时动效这么流畅
还以为是 android.Transition Framework 的一个我不知道的用法..

表现形式上很像新的转场动画特效(Shared elements between activities)

android.Transition Framework can be used for three main things:

  • Animate View elements in transitions between activites (or fragments)
  • Animate shared elements (hero views) in transitions between activities (or fragments)
  • Animate View elements from one activity scene to another.

Github上有个低版本的兼容库 https://github.com/tianzhijiexian/ActivityOptionsICS
使用在Intent上传递Bitmap做到View的传递,不过只有视觉上的效果,因为传过去的并不是View,只是用指定View生成的Bitmap

我自己的ScreenTransitionTool原理也和这个一样…….不过当时没写完…丢下了就有点捡不起来了..

这个时候就觉得 IOS 的 ViewController系统优势的多,Controller切换时可以同时访问前后2个Controller

回归正题

Euclid可以做到这个效果的原因真的很傻逼,因为它的List界面和Detail界面在同一个Activity中….

所以…..剩下就是普通的 ObjectAnimator ……… sad ….

mipmap 目录和drawable 目录有什么区别

我简单总结一下:

使用上没有任何区别,你把它当drawable用就好了。

但是用mipmap系统会在缩放上提供一定的性能优化。

#官方介绍:

Mipmapping for drawables

Using a mipmap as the source for your bitmap or drawable is a simple way to provide a quality image and various image scales, which can be particularly useful if you expect your image to be scaled during an animation.

Android 4.2 (API level 17) added support for mipmaps in the Bitmap class—Android swaps the mip images in your Bitmap when you’ve supplied a mipmap source and have enabled setHasMipMap(). Now in Android 4.3, you can enable mipmaps for a BitmapDrawable object as well, by providing a mipmap asset and setting the android:mipMap attribute in a bitmap resource file or by calling hasMipMap().

 


#应用场景:

If you know that you are going to draw this bitmap at less than 50% of its original size, you may be able to obtain a higher quality by turning this property on. Note that if the renderer respects this hint it might have to allocate extra memory to hold the mipmap levels for this bitmap.

 


#一个应用实例:

Nexus 6
Screen
The Nexus 6 boasts an impressive 5.96” Quad HD screen display at a resolution of 2560 x 1440 (493 ppi). This translates to ~ 730 x 410 dp (density independent pixels).

Check your assets
It has a quantized density of 560 dpi, which falls in between the xxhdpi and xxxhdpi primary density buckets. For the Nexus 6, the platform will scale down xxxhdpi assets, but if those aren’t available, then it will scale up xxhdpi assets.

Provide at least an xxxhdpi app icon because devices can display large app icons on the launcher. It’s best practice to place your app icons in mipmap- folders (not the drawable- folders) because they are used at resolutions different from the device’s current density. For example, an xxxhdpi app icon can be used on the launcher for an xxhdpi device.

res/
mipmap-mdpi/
ic_launcher.png
mipmap-hdpi/
ic_launcher.png
mipmap-xhdpi/
ic_launcher.png
mipmap-xxhdpi/
ic_launcher.png
mipmap-xxxhdpi/
ic_launcher.png # App icon used on Nexus 6 device launcher
Choosing to add xxxhdpi versions for the rest of your assets will provide a sharper visual experience on the Nexus 6, but does increase apk size, so you should make an appropriate decision for your app.

res/
drawable-mdpi/
ic_sunny.png
drawable-hdpi/
ic_sunny.png
drawable-xhdpi/
ic_sunny.png
drawable-xxhdpi/ # Fall back to these if xxxhdpi versions aren’t available
ic_sunny.png
drawable-xxxhdpi/ # Higher resolution assets for Nexus 6
ic_sunny.png

#总结
这个实例总结一下是这样:
Nexus 6 有 493 ppi
它刚好在 xxhdpi和xxxhdpi之间
所以显示的时候需要对xxxhdpi的资源进行缩小
如果你用了mipmap-xxxhdpi,那么这里会对sclae有一个优化,性能更好,占用内存更少。

所以现在官方推荐使用mipmap

It’s best practice to place your app icons in mipmap- folders (not the drawable- folders) because they are used at resolutions different from the device’s current density.

 

关于 Facebook新闻页优化 的分析

关于 Facebook新闻页优化 的分析

#私货

简单一点概括,就是Facebook在ListView页面中,将原来的View-Model-Binder再次拆解成粒度更细的模型。

在原本的View-Model-Binder模型中,依靠将部分的View设置成visiblegone 来控制功能块的显示 (转帖/图片/etc)

在优化过的模型中,原本的View被拆解成更细粒度的小View,然后只加载需要的部分

原文没有提及,但应当是利用

public int getItemViewType(int position) {
return 0;
}

public int getViewTypeCount() {
return 1;
}

实现的。

翻了一下ListView源代码

不同type的View被分类cache在RecycleBin中

public void setViewTypeCount(int viewTypeCount) {
    if (viewTypeCount &lt; 1) {
        throw new IllegalArgumentException("Can't have a viewTypeCount &lt; 1");
    }
    //noinspection unchecked
    ArrayList&lt;View&gt;[] scrapViews = new ArrayList[viewTypeCount];
    for (int i = 0; i &lt; viewTypeCount; i++) {
        scrapViews[i] = new ArrayList&lt;View&gt;();
    }
    mViewTypeCount = viewTypeCount;
    mCurrentScrap = scrapViews[0];
    mScrapViews = scrapViews;
}

参考RecycleBin的setViewTypeCount部分,此处用ArrayList[]来cache所有type的view

不过由于被设置成gone的View本身就不参与meaure和layout,也不会参与draw,只会额外占据一些内存。

所以Facebook做了这些优化以后 性能只提升了10%

在View的性能优化中,我觉得10%太少了,而新的模型复杂度变高了很多。

原本由于ViewType的原因,需要将Model和View的实现细节暴露在 Adapter中,Facebook这个优化实现似乎为了保持其封装性

于是将View-Model-Binder又通过Definition又封装了一层。

综合考虑的话….我觉得意义不大。

优化掉了一些无用View,但是增加了额外的中间层,很难说这样的改动是好是坏。

IOS开发可能更习惯View-Binder写法

Android开发可能会更习惯直接使用ViewHolder去操作

而Facebook这个写法更复杂,增加了额外的Definition层,Binder也根据ViewType被拆解成多个。


考虑一下直接用最大粒度的View来做ViewType区分?

思考了一下….参考Faacebook或微博这个界面…应当是不行的…

同屏最多也就 2~4条 post

直接用Item做type区分的话….由于数据随机性的问题

cache会被经常性的加载和释放…..反而起不到作用…

使用细粒度的子View做type区分的话,在cache上有优势的多..


结论:有用,必要性不足。

鉴于这个Definition模型的复杂度,非微博,facebook,朋友圈类型的复杂Item,并不需要使用此优化方法。

正常使用View-model-binder模型就好了


中文版
http://blog.aaapei.com/article/2015/02/facebookxin-wen-ye-listviewyou-hua

 

基础知识

android系统每隔16.7ms发出一个渲染信号,通知ui线程进行界面的渲染。为了达到流畅的体验,应用程序需要在这个时间内完成应用逻辑,使系统达到60fps。当一个Listview被添加到布局时,其关联的adapter的getView方法将会被回调。在16.7毫秒这样一个时间单元内,可见listitem单元的getView方法将被按照顺序执行。在大多数情况下,由于其他绘图行为的存在,例如measure和draw,getVIew实际分配到执行时间远低于16ms。一旦listview包含复杂控件时,在16毫秒内不能完成渲染,用户只能看到上一祯的结果,这时就发生了掉帧。

Facebook新闻页介绍

Facebook的新闻页是一个复杂的listview控件,如何使它获得流畅的滚动体验一直困扰我们。 首先,新闻页的每一条新闻的可见区域非常大,包含一系列的文本以及照片;其次,新闻的展现类型也很多样,除了文本以及照片,新闻的附件还可包含链接、音频、视频等。除此之外,新闻还可以被点赞、被转载,导致一个新闻会被其他新闻包含在内。当新闻被大量用户转载时,甚至会出现一条新闻占据两个屏幕的情况。加上android用户的机型多为中低端设备,这使我们在16.7ms内完成新闻页的渲染变的非常困难。

新闻页最初架构

在2012年,我们将新闻页从web-view转化成本地控件,在最初的那个版本中,基于View-Model-Binder设计模型,我们为新闻listitem创建了一个自定义StoryView类,这个类有一个bindModel方法,该方法用于和数据进行绑定。代码是这样的:

StoryView的包含的子控件都会有一个bindModel方法,例如HeadVIew通过该方法与其相关的数据进行绑定。

这种设计,代码非常直观清晰,但他的缺点也很明显:

  • listview复用机制不能有效的工作,Android’s recycling mechanism does not work well in this case: Every item in the ListView was usually a StoryView, but once bound to a story, two StoryViews would be radically different and recycling one into the other wasn’t effective.(这一段存疑,直接放原文)
  • 逻辑嵌套:采用bindModel绑定控件和数据,业务逻辑与视图逻辑耦合,导致逻辑类层次非常深;
  • 布局嵌套非常深:不但导致低效的视图渲染,例如新闻被不停的转载的极端场景下还会导致栈溢出;
  • bindModel方法逻辑过重:bindModel方法在当用户滚动列表时被ui线程回调,由于所有的数据解析都在这个方法内,导致该方法耗时

以上这些问题虽有他们单独的解决方法,例如我们可以自己设计一套回收机制解决storyView复用问题。但基于维护成本和开发时间考虑,我们决定进行一次重构。

重构方案

重构工作大约是一年之前开始的,为了解决前一个架构的问题,首先我们决定将一条新闻分隔成多个listview item。例如,新闻的headerview将是一个独立的listitem。这样,我们可以利用android回收机制,HeaderView新闻子控件将被不同的新闻复用。另外,切分成小view也使得内存占用更小,在之前的架构中,Storyview部分的可见会导致这个Storyview被加载到内存中,而现在,粒度更小,只有可见的子控件才会被加载。

另一个大的修改是,我们将视图逻辑和数据逻辑分离,StoryView被分离成两个类: 只负责展现的视图类,以及一个Binder类。视图类仅包含set方法(例如HeaderView包含了setTitle,setSubTitle。setProfiePic等等)。Binder类包含了原来的bindMethod的逻辑,binder类包含三个方法:prepare,bind,unbind。 bind方法调用view的set方法设置数据,unbind清理视图数据,prepare方法在cpu空闲期间做一些预初始化工作,例如进行click事件绑定、数据格式化、创建spannable等等,它会在getView方法之前被调用

我们遇到的技术难点是Binder的设计,由于StoryView被拆分不同的子控件,一条新闻可能会包含多个不同的Binder。而在之前,我们只需要根据视图的树结构进行结构化赋值。因此,我们引进了PartDefinition类,PartDefinition负责维护一条新闻包含哪些子控件、包含Binder的类型以及为新闻创建Binder类,有两种类型的PartDefinition:单个PartDefinition以及PartDefinition集合。

一个新闻在重构之后的PartDefinition结构是这样的:

结论

  • 采取新的架构,内存错误减少了17%,总crash率减少了8%,彻底解决涨溢出问题
  • 渲染时间减少了10%,大新闻场景不再掉帧
  • 精简了原来的自定义回收机制,同时在重构过程中增加了单元测试

 

英文原文:

https://code.facebook.com/posts/879498888759525/fast-rendering-news-feed-on-android/

 

If you work on an Android app (or any touch screen based app actually), there’s a very good chance you have at least one activity based on a ListView. In many cases, it may be the screen of your app where users interact with the app the most. In the case of Facebook for Android, this ListView is your News Feed. Keeping News Feed working well in Facebook’s Android app presents a variety of engineering challenges starting with performance considerations for touchscreens.

Why ListView Gets Complicated

ListView presents an interesting performance problem that wasn’t as dominant on non-touch interfaces - the smoothness of the scrolling experience. Users expect that the content will seamlessly keep pace with their fingers as they scroll on a touchscreen. This need wasn’t as important when using a keyboard and mouse.

To get your list to appear as if it scrolls smoothly, you need to render about 60 frames of it per second. Basic arithmetic translates this challenge into rendering one frame in under 16.7 milliseconds. This is pretty easily done for most frames with the exception of a notable subset. Whenever a new view goes into the viewport in your ListView, the ListView will call its adapter’sgetView method. That method, in turn, has to get a view, and bind all its content into it in less than 16.7ms. (In fact, since other actions have to happen, such as measuring and drawing, there’s even less time). This is easily achievable for simple content, such as the views that contain text only. It becomes challenging when trying to render rich content which contains complicated views — a lot of text and images, for example. Android’s simple tool to solve this is view recycling: The adapter’sgetView method will get an existing convertView to use if one was made before. This trick works very well, but only when your views are similar to each other.

Rendering the Facebook News Feed

Facebook’s News Feed, a popular screen in the Facebook for Android app, is an extreme example of a complicated ListView. This makes delivering a smooth scrolling experience especially hard.

First, each story you see in the feed is pretty tall, and contains several pieces of text and photos. This makes the challenge of binding all of the story’s data without skipping frames significantly harder.

Secondly, there are multiple variations of stories in your feed. Aside from a simple story containing some text and a photo, you can see multiple variations of attachments: a preview for a link, an album, a video, etc. The story might be shared, in which case the story contains another story inside it. Hardest of all, we need to render aggregated stories which means one story can actually be composed of several stories that are related. A good example of this is when many of your friends wish you a happy birthday. These aggregated stories are the most challenging to render, as one of them can easily be twice as tall as the screen of the device.

As an extra challenge, the typical Android phone is not a high-end device. So the amount of computations you can fit in under 16.7ms is likely less than the amount on the majority of phones you develop on.

How it used to work

Around 2012, we started working on converting News Feed from a web-view based solution to native rendering. We knew this was going to be a complicated task due to the sheer number of features news feed supported already at that time. Our design was based on the following concept: For every piece of a story in the feed, we created a custom view class. That custom view class would have a method called bindModel which would take the object this view was supposed to display and update the view to display it.

So, this meant to display a story we had a StoryView class, which among other things, had aHeaderView inside it to display the header of the story.

This approach was simple and intuitive for finding the right piece of code or knowing where to add it, but had various drawbacks:

  • Android’s recycling mechanism does not work well in this case: Every item in the ListView was usually a StoryView, but once bound to a story, two StoryViews would be radically different and recycling one into the other wasn’t effective.
  • Complicated hierarchies: Using a bindModel method inside views meant coupling the business logic with the layout logic. Once something as complicated as a view for displaying a story existed, it was natural to subclass it when you needed a variation of that logic. This resulted in a complicated and hard to navigate hierarchy of classes, which is something to avoid.
  • Deep-View hierarchy: Logically grouping things meant putting them inside the same view which resulted in a very deep hierarchy of views. This means worse performance for Android when measuring views, and in extreme cases meant actual crashes due to stack overflows of the number of views. This cases tended to happen in something as an aggregated story, where the recursive “story in a story” rendering caused a very deep hierarchy.
  • Computationally-intensive method: There is no nice and consistent way to execute code beforebindModel is called. Since bindModel is called while the user is scrolling, it’s a pretty bad time to do heavy work. Since this is the place where you unpack a complicated story object into the various parts, that would not always be the case, which made bindModel a computationally intense method to run for some of the views.

All of these problems were solvable, but each required its own custom solution, which took time, and made maintenance more complicated. For example, to solve the recycling problem we had to implement our own custom recycler to work on top of the list view. With that in mind, we decided to rewrite our rendering code to avoid these pitfalls.

Making News Feed Scrolling Smoother

About a year ago we felt it was the time to invest in a new architecture for our rendering code for News Feed. We knew we wanted to avoid the pitfalls of the previous design, and try to get rid of our more fragile code. For that purpose we considered a new idea: Splitting each story in the News Feed to several items in the ListView. Using this idea, the header part of a story will be its own item in the ListView. This nice trick results in various advantages right away. First, this makes Android’s recycling effective again. If before two custom views for a story were two different to have one recycled into the other, now, recycling is happening on a sub story level.

So a story’s header view is being recycled with a different header, and these have the same layout. The second big advantage is that splitting the views lets us store less views in memory, and bind a story over several frames. If before a huge aggregated story presented a difficult task of binding it without skipping a frame, now this story is split into many parts, and only one of those needs to be bound during frame. This effectively amortizes the binding time for a story over several frames, resulting in less skipped frames.

The other idea we incorporated to the design is decoupling the binding logic from the custom Views themselves. For this purpose we basically split our previous custom view into two classes: A simpler “dumb” custom view, and a Binder class. The custom view is now limited to a basic set of setters (such as “setTitle”, “setSubtitle” and “setProfilePic” for the header) and is unaware of a story object. The binder class is the replacement for the bindModel method. It has three methods: prepare, bind, and unbind. The bind method takes a view and sets the right info from the story on it, the unbindreverses this and does cleanup which might be necessary (usually it’s not needed) and lastly, theprepare method is meant to solve the issue of performing hard work during binding time. It is called before the first time a binder is bound, and intelligently scheduled when there’s free CPU time on the UI thread. This makes it ideal for allocating click listeners, formatting strings, building spannables and so forth.

To make our adapter work, we just need to generate the right list of binders. This is not a simple task; there are many types of binders, and each story has a different number of them according to the parts it should render. In the previous design, this was solved by the hierarchy of views, but now that there’s no “one view” per story we needed to create it in a different way. We decided to do this by creating a PartDefinition class to define each possible part of a story. There are two types of part definitions: A single part definition which defines what views it needs, whether it is needed for a specific story, and how to create the correct binder for it. The second type is a group part definition that lets us logically group several parts into one.

Using these classes we rebuilt the hierarchy of parts of a story. Now to generate the list of binders for a story, we simply start from the root part and recursively expand it to the list of single parts, which we then use to generate binders.

This rewrite effort has resulted in many benefits:

  • The number of out-of-memory errors experienced by people using Facebook has been reduced by 17% and the number of total errors was reduced by 8%. Some errors, such as stack overflows in the view hierarchy have disappeared.
  • The maximum time it takes to render a frame was reduced by 10%, after additional optimizations and removal of custom recycling code that were no longer needed. Additional simplifications are expected to improve by such amounts once more. Big jumps that resulted from loading a tall complicated story have disappeared.
  • We were able to simplify reusing our feed code with different layouts, which helped in the creation of the stand alone app for Facebook Groups on Android.
  • The new design has lent itself to improving our code quality. More teams can contribute to News Feed while being sandboxed from other teams working on parallel features. We also used this opportunity to make sure our code is testable and well covered. The new feed code has a line coverage with unit tests of 70% compared to 17% in the old code.

This has been a great undertaking to replace such a core piece of code seamlessly, but the hard work was worth it as it paid of both in improving the performance, and the reliability and maintainability of the code.


Android Studio Gradle构建速度优化

1.

https://www.timroes.de/2013/09/12/speed-up-gradle/

create a file named gradle.properties in the following directory:

  • /home/<username>/.gradle/ (Linux)
  • /Users/<username>/.gradle/ (Mac)
  • C:\Users\<username>.gradle (Windows)
    Add this line to the file:
org.gradle.daemon=true

From now on Gradle will use a daemon to build, whether you are using Gradle from command line or building in Android Studio. You could also place the gradle.properties file to the root directory of your project and commit it to your SCM system. But you would have to do this, for every project (if you want to use the daemon in every project).

关于这个写法有几个变种,都尝试了一下,并没有用。

参考Android Studio配置选项

speed-up-gradle

Android Studio 1.1.0+版本中该配置已经默认打开。


2.
用命令行Build

可以参考这个讨论串
https://plus.google.com/u/0/+RicardoAmaral/posts/e9PG6vSN5w3

gradle assembleDebug
实测速度快50%左右 原理不明
回头写个脚本 build完成再自动安装运行应该就好了

更新下

gradle installDebug 就可以了,不用自己在gradle里搞task了,省事.
明天测试下性能


3.
Android模块化编程之引用本地的aar
http://stormzhang.com/android/2015/03/01/android-reference-local-aar/

目前把项目中引用的Library都打成了本地aar,减少编译的文件,编译速度显著加快


4.
我觉得TMD还是把公司的破电脑扔了换个新的才能从根本上解决问题