Android项目复盘1

我的主页:chengang.plus/java

文章将会同步到我的微信公众号:Android部落格android

一、商城项目

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

  • 缘由:首页包含了精选,banner,秒杀,热卖列表,可是每个ViewType没有在RecyclerView中设置各自的类型,致使缓存的时候当作一整ViewHolder缓存,从而总体内存占用太高。尤为底部的热卖列表上拉加载的时候,显得尤其显著。

1.1.1 源码追溯

RecyclerView.Recyclerc++

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.RecycledViewPoolshell

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;
}
复制代码

缓存分两个区域:json

  • mCachedViews是一个List类型;
  • SparseArray中的ScrapData包含了ArrayList,分ViewType存放ViewHolder,相同ViewType的ViewHolder放在一个List中,当取一个缓存出来的同时remove。

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

1.1.2 解决办法

  • 首页的多个视图类型拆分为不一样的ViewType,分不一样的视图类型加载。
  • 根据视图中商品icon图片大小,与服务端协商减少商品列表中icon图片的分辨率;同时本地存放的图片必须放置在合理的drawable文件夹中,由于文件夹对应的设备像素密度与机器屏幕像素密度越接近,内存占用会越小。

下边解释缘由。缓存

1.1.2.1 Bitmap内存计算

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

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所处文件的密度。)

  • ARGB_8888: 每一个像素4字节. 共32位,默认设置。
  • Alpha_8: 只保存透明度,共8位,1字节。
  • ARGB_4444: 共16位,2字节。
  • RGB_565:共16位,2字节,只存储RGB值。

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

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

1.1.2.2 Bitmap内存模型
  • Android 3.0 (API level 11)

从这个版本开始,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的内存。

  • Android 8.0以前

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

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

  • Android 8.0以后

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

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

  • 在RecyclerView Adapter的onViewRecycled方法中,释放图片:
@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 引导页到首页中间过渡时间长

developer.android.com/topic/perfo…

zhuanlan.zhihu.com/p/91226153

juejin.im/entry/5b813…

1.2.1 Application到Activity加载流程

Activity启动流程

总结上图的流程就是:

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

1.2.2 启动分析

  • 冷启动。冷启动是指应用从头开始启动:系统进程在冷启动后才建立应用进程。发生冷启动的状况包括应用自设备启动后或系统终止应用后首次启动。这种启动给最大限度地减小启动时间带来了最大的挑战,由于系统和应用要作的工做比在其余启动状态下更多。

  • 热启动。应用的热启动比冷启动简单得多,开销也更低。在热启动中,系统的全部工做就是将您的 Activity 带到前台。若是应用的全部 Activity 都还驻留在内存中,则应用能够无须重复对象初始化、布局扩充和呈现。

  • 温启动。温启动涵盖在冷启动期间发生的操做的一些子集;同时,它的开销比热启动多。有许多潜在状态可视为温启动。例如:

用户退出您的应用,但以后又从新启动。进程可能已继续运行,但应用必须经过调用 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
复制代码
  • ThisTime : 最后一个 Activity 的启动耗时(例如从 LaunchActivity --> MainActivity「adb命令输入的Activity」 , 只统计 MainActivity 的启动耗时)
  • TotalTime : 启动一连串的 Activity 总耗时.(有几个Activity 就统计几个)
  • WaitTime : 应用进程的建立过程 + TotalTime .

图片来源juejin.im/entry/5b813…

  • 在第①个时间段内,AMS 建立 ActivityRecord 记录块和选择合理的 Task、将当前Resume 的 Activity 进行 pause.
  • 在第②个时间段内,启动进程、调用无界面 Activity 的 onCreate() 等、 pause/finish 无界面的 Activity.
  • 在第③个时间段内,调用有界面 Activity 的 onCreate、onResume.

1.2.4 解决问题

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

1.2.4.1 请求数据统一整合

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

  • 第一步,将以前各个负责人的数据请求框架集合到一个类中,综合统一调度。统一Json解析方法,全部的json解析挪入IO线程处理,序列化成类以后统一返回。

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

  • 第二步,首页数据分多个接口提供,每个接口一个负责人,致使每个负责人在首页启动的时候都去请求数据,没有作到统一调度,并且部分页面的数据在IO线程请求完毕以后,调度到UI线程作解析,明显拖慢了加载速度
  • 第三步,对首页统一返回的数据作拆分,对优先级比较低的数据,单独作一个接口,待首页加载完成以后或页面展现时再展示
  • 第四步,将在Application中预先加载首页数据挪到闪屏页加载,充分利用闪屏页延迟等待的2000ms的时间
  • 第五步,将没必要要的SDK初始化挪到首页展现完成以后初始化或使用前初始化
1.2.4.2 视图xml优化
  • 将闪屏页的图片从xml的ImageView android:src中移除,加快xml inflate速度,并在Activity的onCreate方法中经过图片工具类加载,这样有必定几率通缓存中加载,避免每次都须要解码。而解码又须要消耗一些内存,还可能致使OOM。
  • 检查闪屏页和首页的xml视图层级,将过渡绘制的页面提出来重点优化,对没必要要的层级删除,对没必要要的背景设置为null或透明。
  • 对于自定义的View,重点检查onDraw方法,避免对象的建立
1.2.4.3 其余一些优化
  • SharedPreference不能写入大量数据做为value,只能写入一些flag等标识性变量,由于SharedPreference在初始化的时候会从Disk加载数据,这里是阻塞式的。而若是以前存有大量的数据在里面会致使阻塞主线程。在获取Editor往里面提交数据的时候,又会去等待SharedPreference初始化完成。因此这里一方面致使SharedPreference初始化慢,IO操做会阻塞主线程;另外一方面又有可能由于等待引发ANR。

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刷新时间长,刷新频繁

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();
}
复制代码

能够看到最终会致使从新请求渲染帧,更新视图。

解决方案是:

  • 将在顶级视图的setState方法下放到各个须要更新的子视图中,由子视图控制刷新
  • 将没必要要的StatefulWidget改为StatelessWidget,避免没必要要的刷新
  • 当子视图须要状态更新可是层级较多时,引入InheritedWidget。看看InheritedWidget的大体工做流程:
//第一步
@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初始化阶段。

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

  • 第二步,从InheritedWidget获取数据阶段。

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

  • 第三步,通知子Element更新。

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

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

相关文章
相关标签/搜索