Android项目复盘1

2020-06-16  本文已影响0人  cg1991

个人主页:https://chengang.plus/

文章将会同步到个人微信公众号:Android部落格

1、商城项目

1.1 RecyclerView首页加载商品item内存占用过高

1.1.1 源码追溯

RecyclerView.Recycler

void recycleViewHolderInternal(ViewHolder holder) {
    boolean cached = false;
    boolean recycled = false;
    if (forceRecycle || holder.isRecyclable()) {
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
            // Retire oldest cached view
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK
                    && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                // when adding the view, skip past most recently prefetched views
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                        break;
                    }
                    cacheIndex--;
                }
                targetCacheIndex = cacheIndex + 1;
            }
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    }
}

RecyclerView.RecycledViewPool

public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
        return;
    }
    scrap.resetInternal();
    scrapHeap.add(scrap);
}
SparseArray<ScrapData> mScrap = new SparseArray<>();

static class ScrapData {
    final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
    int mMaxScrap = DEFAULT_MAX_SCRAP;
    long mCreateRunningAverageNs = 0;
    long mBindRunningAverageNs = 0;
}

public ViewHolder getRecycledView(int viewType) {
    final ScrapData scrapData = mScrap.get(viewType);
    if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
        final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
        for (int i = scrapHeap.size() - 1; i >= 0; i--) {
            if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                return scrapHeap.remove(i);
            }
        }
    }
    return null;
}

缓存分两个区域:

到这里可以明白,当首页整个作为一个Viewtype类型的时候,会缓存一个很大的ViewHolder对象到mCachedViews或RecycledViewPool中。

1.1.2 解决办法

下边解释原因。

1.1.2.1 Bitmap内存计算

本地加载图片时的各种decode方法最终到了BitmapFactory.cpp的doDecode()方法中,如下:

BitmapFactory.cpp

static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, jobject padding, jobject options) {
    if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
       const int density = env->GetIntField(options, gOptions_densityFieldID);
       const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
       const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
       if (density != 0 && targetDensity != 0 && density != screenDensity) {
          scale = (float) targetDensity / density;
       }
    }

    // Determine the output size.
    SkISize size = codec->getSampledDimensions(sampleSize);
    
    int scaledWidth = size.width();
    int scaledHeight = size.height();
    bool willScale = false;
    // Apply a fine scaling step if necessary.
    if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
            willScale = true;
      scaledWidth = codec->getInfo().width() / sampleSize;
            scaledHeight = codec->getInfo().height() / sampleSize;
    }
    
    // Scale is necessary due to density differences.
    if (scale != 1.0f) {
       willScale = true;
       scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
       scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
    }
    
    const float sx = scaledWidth / float(decodingBitmap.width());
    const float sy = scaledHeight / float(decodingBitmap.height());
    
    SkCanvas canvas(outputBitmap);
    canvas.scale(sx, sy);
    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);

从源码可以看出scaledWidth经过两次计算,一次是如果sampleSize不等于1的时候计算缩放宽高,等于原宽高分别除以采样倍数;另外一次是如果目标屏幕密度和当前图片所处文件夹的密度不一致的话,计算出:

scale = targetDensity / density

(比如机器当前是xxhdpi,对应480,而图片放置在xhdpi中,对应320,就会算出一个大于1的拉伸系数)

如果scale不等于1,用第一次计算的

scaledWidth * scale + 0.5,scaledHeight * scale + 0.5

可以看到分两步,一步是用最初的图片大小除以采样系数;一步是根据屏幕密度计算出来的拉伸系数然后乘以这个系数

不过具体在做缩放操作的时候缩放因子等于两次计算之后的宽高分别处以原始宽高。可见对于设置采样率可以节省部分内存。

最后实际的占用大小:

width = (originWidth / sampleSize) * (targetDensity / density) + 0.5

height = (originHeight / sampleSize) * (targetDensity / density) + 0.5

totalSize = width * height * 像素位

(targetDensity是手机实际密度,等于宽平方 + 高平方开根号,处于屏幕对角线长度,density是图片在App所处文件的密度。)

getRowBytes()返回的是每行的像素值,乘以高度就是总的像素数,也就是占用内存的大小。

getAllocationByteCount()与getByteCount()的返回值一般情况下都是相等的。只是在图片 复用的时候,getAllocationByteCount()返回的是复用图像所占内存的大小,getByteCount()返回的是新解码图片占用内存的大小。

1.1.2.2 Bitmap内存模型

从这个版本开始,bitmap的ARGB数据(像素数据)和bitmap对象一起存在Dalvik的堆里了。这样bitmap对象和它的ARGB数据就可以同步回收了。

后续Android又引入了BitmapFactory.Options.inBitmap字段。

如果设置了这个字段,bitmap在加载数据时可以复用这个字段所指向的bitmap的内存空间。新增的这种内存复用的特性,可以优化掉因旧bitmap内存释放和新bitmap内存申请所带来的性能损耗。

但是,内存能够复用也是有条件的。比如,在Android 4.4(API level 19)之前,只有新旧两个bitmap的尺寸一样才能复用内存空间。Android 4.4开始只要旧bitmap的尺寸大于等于新的bitmap就可以复用了。

这样GC无法知道当前的内存情况是否乐观,大量创建bitmap可能不会触发到GC,而Native中bitmap的像素数据可能已经占用了过多内存,这时候就会OOM,所以推荐在bitmap使用完之后,调用recycle释放掉Native的内存。

Bitmap的内存分配在dalvik heap,Bitmap中有个byte[] mBuffer,其实就是用来存储像素数据的,它位于java heap中,通过在native层构建Java Bitmap对象的方式,将生成的byte[]传递给Bitmap.java对象。

像素数据就和bitmap对象一起都分配在堆中了,一起接受GC管理,只要bitmap置为null没有被强引用持有,GC就会把它回收掉,和普通对象一样。

Bitmap像素内存的分配是在native层直接调用calloc,所以其像素分配的是在native heap上,并且还引入了NativeAllocationRegistry机制。

Bitmap引入了NativeAllocationRegistry这样一种辅助自动回收native内存的机制,依然不需要用户主动回收了,当bitmap的Java对象被回收后,NativeAllocationRegistry辅助回收这个对象所申请的native内存。

@Override
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
    try {
        if (!((Activity) context).isDestroyed() && !((Activity) context).isFinishing()) {
            ImageView img = holder.itemView.findViewById(R.id.goods_img);
            img.setImageDrawable(null);
            Glide.with(context).clear(img);
        }
    } catch (Exception e) {
        MyLog.d(TAG, "recycle fail:" + e.getLocalizedMessage());
    }
}

1.2 引导页到首页中间过渡时间长

https://developer.android.com/topic/performance/vitals/launch-time?hl=zh-cn

https://zhuanlan.zhihu.com/p/91226153

https://juejin.im/entry/5b8134cdf265da434a1fce4b

1.2.1 Application到Activity加载流程

Activity启动流程

总结上图的流程就是:

Application的构造器方法——>attachBaseContext()——>onCreate()——>Activity的构造方法——>onCreate()——>配置主题中背景等属性——>onStart()——>onResume()——>测量布局绘制显示在界面上。

1.2.2 启动分析

image

用户退出您的应用,但之后又重新启动。进程可能已继续运行,但应用必须通过调用 onCreate() 从头开始重新创建 Activity。
系统将您的应用从内存中逐出,然后用户又重新启动它。进程和 Activity 需要重启,但传递到 onCreate() 的已保存实例状态包对于完成此任务有一定助益。

1.2.3 测量启动时间

adb shell am start -W [packageName]/[packageName.MainActivity]

输出如下:

E:\data_parse>adb shell am start -S -W com.xx.xx.xx/.activity.MainActivity
Stopping: com.xx.xx.xx
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.xx.xx.xx/.activity.MainActivity }
Status: ok
Activity: com.xx.xx.xx/.activity.MainActivity
ThisTime: 1136
TotalTime: 75246
WaitTime: 1179
Complete
image

图片来源https://juejin.im/entry/5b8134cdf265da434a1fce4b

1.2.4 解决问题

从两方面入手,想办法缩短Application消耗的时间;缩短Activity消耗的时间。

1.2.4.1 请求数据统一整合

我们的项目中有各种SDK的初始化,包括友盟,百川,开普勒,Glide,分享等。

json解析的过程存在json字符遍历,而商城类项目从服务端返回的数据上百k,有些json结构非常复杂,比较耗时

1.2.4.2 视图xml优化
1.2.4.3 其他一些优化

SharedPreferencesImpl

private final Object mLock = new Object();

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

    @Override
public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked();
    }

    return new EditorImpl();
}

public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

在这里可以看到,加锁的对象是mLock,当loadFromDisk方法执行完毕之后,才会执行mLock.notifyAll();,至此,其他的代码才会获得执行时机。尤其是后续edit,以及put/get操作的时候。

1.3 flutter版本Widget刷新时间长,刷新频繁

http://fluttersamples.com/

当在StatefulWidget中调用setState的时候,会导致当前Widget下所有Widget树刷新,这种情况如果遇上复杂的布局,肯定是不可想象的,先看看调用setState的时候发生了什么,伪代码如下:

@protected
void setState(VoidCallback fn) {
    final dynamic result = fn() as dynamic;
    _element.markNeedsBuild();
    
     scheduleFrame();
     
     void handleDrawFrame() {};
     
     void drawFrame() {};
     
     rebuild();
     
     preformRebuild();
     
     build();
     
     updateChild();
     
     update();
}

可以看到最终会导致重新请求渲染帧,更新视图。

解决方案是:

//第一步
@override
void _updateInheritance() {
    final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
    if (incomingWidgets != null)
      _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
    else
      _inheritedWidgets = HashMap<Type, InheritedElement>();
    _inheritedWidgets[widget.runtimeType] = this;
}

//第二步
@override
T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
}

@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
}

@override
void updateDependencies(Element dependent, Object aspect) {
    setDependencies(dependent, null);
}

@protected
void setDependencies(Element dependent, Object value) {
    _dependents[dependent] = value;
}

//第三步
@protected
void updated(covariant ProxyWidget oldWidget) {
    notifyClients(oldWidget);
}

@override
void notifyClients(InheritedWidget oldWidget) {
    for (final Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
}

void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    dependent.didChangeDependencies();
}

@mustCallSuper
void didChangeDependencies() {
    markNeedsBuild();
}

在这个阶段每一个Element在mount的过程中会调用_updateInheritance方法,生成一个HashMap _inheritedWidgets。这里比较取巧的是,当父类已经存在的时候,直接在父类的_inheritedWidgets里面追加,而runType就是他的key,所以可以轻松找到InheritedWidget。

InheritedWidget的子Widget调用它对外暴露的of方法时,通过调用dependOnInheritedWidgetOfExactType方法返回InheritedWidget自身。这里从第一步的_inheritedWidgets中通过runType找到这个对象,然后调用它的setDependencies方法,将子Widget的Element作为依赖项加入到一个HashSet _dependents中。

当InheritedWidget的数据发生变化时,会触发渲染树更新,当调用它的update方法更新Element的时候,会遍历上一步_dependents中保存的依赖Element,并重建这些Element。

透过以上步骤,我们可以发现不论InheritedWidget与需要依赖它数据的Widget中间隔了多少层级,只要InheritedWidget数据发生变化,都能通知依赖它的Widget重绘。

上一篇下一篇

猜你喜欢

热点阅读