使用适配器在多个水平 LinearLayout 中有效地膨胀大量视图

Efficiently Inflating a lot of Views within several Horizontal LinearLayout with Adapters

我有一个关于如何提高大型水平线性布局的性能的问题。

我正在创建一个类似于 table 的视图,可以包含 50 到 2,500 个条目。每个条目都是一个包含带有一些简单文本的 TextView 的 LinearLayout。我已经利用 LinearListView library 实现了设计。该库允许将 ListAdapter 绑定到 LinearLayout 以在水平或垂直方向上显示视图。

我目前实现它的方法是利用其中的两个 LinearListView。一种是垂直的,数据由水平的 LinearListViews 组成。这通过创建 table 视图提供了所需的输出。然后,我将 table 包裹在带有 Verticle ScrollView 的 Horizo​​ntal ScrollView 中,以便可以平移 table(在 up/down 或 left/right 中滚动)。

此布局的问题在于适配器 getView() 仅在视图初始化时被调用(第一次扩充第一个 linearList 时它扩充每个视图)。一旦每个视图都被膨胀, getView() 方法就不会在列表中滚动时再次调用(当然它滚动得很好,因为它已全部加载)。膨胀所花费的总时间并不重要,但它连续膨胀 table 中的每个项目这一事实锁定了主 UI 线程。我需要查看 "lazy load" table 中的每个项目而不阻塞 UI 线程。

我有一个屏幕截图,但我没有足够的声誉来 post 它也没有足够的声誉来使用两个 link。我会尝试在评论中放一张外部照片link。 (参考屏幕截图)table 数据是在一组异步任务中生成的,其中每个 table 项都是以毫秒为单位的当前时间(我知道 table 行不是由于异步性质而订购,稍后会修复)。这个实际的应用程序除了演示这个库之外没有任何用途。

我添加了 "Change Data Randomly" 按钮,它会创建一个随机点 (int x,int y) 并生成一个随机字符串,并将 (x,y) 处的单元格替换为该字符串。这几乎是通过调用该细节适配器的 getView() 方法立即发生的。所以访问这个 table 非常快!同样,正是初始膨胀锁定了主线程 UI。

要总结的几个重要注意事项:

我发现这个 application (TV SideView) 创建了一个相当大的 table 视图,加载非常好。我最终会想要实现与此类似的东西(查看 "Program Guide" 页面以查看实际的 table)。它加载了一堆单元格,你仍然可以使用 UI(当它第一次打开时拖动 table,你会看到单元格正在加载)。

我会继续努力,post 支持我发现的任何新东西。

如有任何建议和帮助,我们将不胜感激!非常感谢您的宝贵时间!

-埃文

由于您关心 UI 性能,您可以使用 AsyncTask 抽象 class,常用 Google 推荐。 AsyncTask 在与 UI 不同的线程上运行。要使用它,您必须创建一个 class 来子 class 它。 Google 网页@ AsyncTask,为了您的方便。

我找到的代码示例位于 Using an AsyncTask to populate a ListView。 在代码中,注意 getItemLists 扩展了 AsyncTask。在 class 调用 setListAdapter 方法时覆盖 onPostExecute(),您可能很熟悉。

上面 link 中的代码片段:

private class getItemLists extends
            AsyncTask<Void, String, ArrayList<Item>> {
...
   @Override
   protected String doInBackground(String... params) {
   // Good place to add code for time consuming work, no UI access though.
   ...
   }

   @Override
   protected void onPostExecute(ArrayList<Item> result) {
   super.onPostExecute(result);
   ...
   }

我从来没有用过这个 AsyncTask,但我可能会用到。请随时通知我们。祝你好运...

我想通了:)

@TheOriginalAndroid 的回答是一个绝妙的想法和回应!非常感谢您的时间和帮助。我实际上已经开始实施 AsyncTask Manager 并在昨天早上完成了它。

我通过创建一个名为 AsycnGridManager 的 class 来解决这个问题,它将管理负责绘制视图的 asyncTasks 组。这是相当多的代码,但我在评论中进行了非常详细的介绍。这不是实际代码,而是 shell 以显示其工作原理的概述。我没有编译它所以请不要把它当作钻石。这个 class 应该从你的主 activity 或负责它的片段中的主线程创建和启动。

/**
 * This class will manage a view and load it asynchronously.
 * In particular, this view will manage a linearLayout in 
 * 2D space. IE. One verticle linear layout with a horizontal 
 * linearLayout at each row.
 * @author Evan Boucher
 */
public class AsyncGridManager {

    /**
     * This is the core number of Threads in the pool.
     * You should probably consider checking the 
     * system for the number of cores the device has.
     * I currently use 4 as it fits my needs.
     */
    private static final int NUM_OF_THREADS_IN_POOL = 4;

    /**
     * The max number of threads that can exist in the pool at one time.
     */
    private static final int MAX_NUM_OF_THREADS_IN_POOL = 10;

    /**
     * The max number of tasks that the queue can hold for the 
     * pool
     */
    private static final int MAX_NUM_OF_TASKS_IN_QUEUE = 150;

    /**
     * The max keep alive time for a thread task in the pool.
     * This should be longer than your longest task. If you have
     * a long UI task in each thread (you are probably doing
     * to much to begin with!) then the task may get stopped
     * before it finishes.
     */
    private static final int THREAD_KEEP_ALIVE_TIME = 4000;

    /**
     * The minimum time to wait to paint a single EPG item.
     * This means that a block will never be painted any faster
     * than this number in Milliseconds.
     */
    private final int MIN_WAIT_TIME_TO_PAINT = 100;

    /**
     * The max time an async task will sleep before painting on the
     * UI thread.
     */
    private final int MAX_WAIT_TIME_TO_PAINT = 1000;

    /**
     * The thread pool that the async tasks within this class will
     * pull from. This is defined by the above varaibles.
     */
    private ThreadPoolExecutor mThreadPool;

    /**
     * The queue of tasks that the thread pool will pull from.
     * The size is fairly large as I don't much care about memory 
     * usage right now. Once the queue fills up it will not add
     * anymore tasks. Be aware of that! So tasks can be lost or
     * cause a thread to block (if you add the tasks on the main
     * thread).
     */
    private BlockingQueue taskQueue;

    /**
     * The thread that this manager will run on as to not block the main thread.
     */
    public Thread mGridManagerThread;

    /**
     * The Grid map object that is the underlying data for this grid.
     * Each key is a row and each value is a list for the columns in that
     * row.
     */
    private Map<String,List<CustomObject>> mGridMap;
    //Number of rows in the table (size of the mGridMap)
    private int mNumOfRows;
    //Get the rootView that is already inflated. This is what we will add to.
    private LinearLayout mRootGridView;
    //The Android activity context that this special async manager is attached to.
    private Context mContext;

    /**
     * Creates and initializes this class.
     *
     */
    public AsyncGridManager(Context context, LinearLayout rootView, Map<String,List<CustomObject>> gridMap) {

        //Create a new taskqueue for the EPGblocks.
        taskQueue = new ArrayBlockingQueue<CreateEPGTableRowTask>(MAX_NUM_OF_TASKS_IN_QUEUE);

        //Create a new threadpool for the tasks.
        poolExecutor = new ThreadPoolExecutor(NUM_OF_THREADS_IN_POOL, MAX_NUM_OF_THREADS_IN_POOL, THREAD_KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, taskQueue);
        this.mGridMap = gridMap;
        /*
         * We can only get the number of rows as that is predefined 
         * by this datastructure (won't know how many columns until we get to the row).
         */
        this.mNumOfRows = mGridMap.size();

        this.mContext = context;
        /*
         * The RootView should be a LinearLayout in my case and already be inflated!
         */
        this.mRootGridView = rootView
    }
    /**
     * Tell the async manager to start loading the tasks into the queue.
     * It loads on a seperate thread to make this completely async.
     */
    public void startAsyncLoading() {
        /*
         * It is important here to note that we should inflate the mRootGridView
         * This way adding views to it will be async on the UI thread.
         */
        mGridManagerThread = new Thread(new AsyncGridLoaderRunnable());
        mGridManagerThread.start();
    }

    /**
     * The runnable for this manager to generate 
     */
    public class AsyncGridLoaderRunnable extends Runnable {

        @Override
        public void run() {
            //A for loop to go through the size of the rows 
            for (int i = 0; i < mNumOfRows; i++) {
                //For each row, lets make a AsyncTask to generate and paint that row. You need to make a new one everytime.
                CreateRowAsyncTask rowAsyncTask = new CreateRowAsyncTask(i);
                /*
                 * I pass i in here so that you could also get the rowIndex as a parameter too if we want.
                 * This adds the task to the taskQueue for this pool to execute.
                 */
                rowAsyncTask.executeOnExecutor(poolExecutor, i);
            }
        }
    }
    /**
     * Async Task that will create and print a row
     * from the map.
     */
    public class CreateRowAsyncTask extends AsyncTask {
        //Random generator to force tasks to sleep for random periods.
        private Random mRandomGenerator;
        //The row index that this task is responsible for painting and managing.
        private int rowIndex;
        //The horizontal linearlayou that represents this row. Might want to add it to a list so we can reference it later.
        private LinearLayout singleRowLayout;

        //The local reference to the list of columns for this row.
        private List<CustomObject> columnList;

        public CreateRowAsyncTask(int rowIndex) {
            this.mRandomGenerator = new Random();
            this.rowIndex = rowIndex;
            //Create the linearlayout for the row.
            singleRowLayout = new LinearLayout(mContext);
            //Set it to horisontal to be a row.
            singleRowLayout.setOrientation(LinearLayout.HORIZONTAL);
            //Get a reference to this rows list of columns.
            columnList = mGridMap.get(rowIndex);
        }
        @Override
        protected Object doInBackground(Object... arg0) {
            /*
             * Here you could do some background stuff to setup objects /views.
             * I am going to assume you have some method to generate the view
             * from our CustomObject (the items within the list for the rows).
             */
            //Lets tell the UI thread to add our row real quickly (remember the root view was already inflated)
            mRootGridView.addView(singleRowLayout);

            /*
             * Due to the Async nature we need to draw each row together.
             * If we dont, EPG blocks will be out of order (not guaranteed order).
             * Uses onProgressUpdate() to paint each block in the row.
             */
            CustomObject columnObject;
            for (int i = 0; i < columnList.size(); i++) {
            //Lets save a reference to the object we want to add to the row we are on
            columnObject = columnList.get(i);

                /*
                 * The customView we are adding. This assumes that the columnObject createView() method
                 * will create a new LinearLayout (or View of some type) which we will add to this row.
                 * You could put the createView() call directly in the publishProgress() method for
                 * ease, but I left it out to show the custom view creation.
                 * Be sure that the createView() does not handle any inflated views (these must be 
                 * accessed on the UI thread).
                 */
                CustomView newViewToAddAsColumn = columnObject.createView();
                //Create each row and use ProgressUpdate to paint it.
                publishProgress(newViewToAddAsColumn);
                try {
                    /*
                     * Sleep the task for a random period of time, this way the view is not loading all at once.
                     * This is one strategy, there are plenty of other Async Loading strategies
                     */
                    Thread.sleep(mRandomGenerator.nextInt(MAX_WAIT_TIME_TO_PAINT - MIN_WAIT_TIME_TO_PAINT) + MIN_WAIT_TIME_TO_PAINT);

                } catch (InterruptedException e) {
                    Log.e(TAG, "ERROR! AsyncTask failed to wait!!!");
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

        @Override
        protected void onProgressUpdate(Object... values) {
            //Get the customView and add it to the row.
            CustomView customViewToAdd = (EpgEventView) values[0];
            //Add the customView to the row. We assume that the params for the view are within the customView.
            singleRowLayout.addView(customViewToAdd, customViewToAdd.getParams());
        }



     }

   }

我没有 运行 这段代码,所以更多的是将它用作示例而不是完美的解决方案。此代码将向 rootView 异步添加视图,而不会阻塞 UI 体验。 :)

享受,

-埃文

我在使用手风琴样式布局时能够做到这一点,其中每个折叠项目也包含列表的一个子集,因为这种布局不适用于 RecyclerView Viewholder 模式。我使用 Concurrent 作为 Asynctask 的替代品,然后在 doInBackground 部分所有 Glide 获取图像和 addView() 调用使用 new Handler(Looper.getMainLooper()).post(() -> {//Your Code}); 包装,因为 Glide 和添加视图需要它是 运行在 UI 线程上。每个布局的添加将在屏幕上一个一个地看到,但好在 Choreographer 不再跳帧。

这是我的代码看起来像

public class GenerateLayoutAsync extends BaseConCurrentTask<Object> {

    private final WeakReference<FragmentActivity> activityReference;
    private final LinearLayoutCompat linearLayoutCompat;
    private final List<GroupMatch> groupMatchList;

    public GenerateLayoutAsync(FragmentActivity context, LinearLayoutCompat linearLayoutCompat, List<GroupMatch> groupMatchList) {
        this.activityReference = new WeakReference<>(context);
        this.linearLayoutCompat = linearLayoutCompat;
        this.groupMatchList = groupMatchList;
    }

    @Override
    public void setUiForLoading() {


    }

    @Override
    public Object call() {

        for (int i = 0; i < groupMatchList.size(); i++) {

            GroupMatch groupMatch = groupMatchList.get(i);

            AppCompatTextView title;
            LinearLayoutCompat container;

            View itemView = LayoutInflater.from(this.activityReference.get()).inflate(R.layout.group_card, linearLayoutCompat, false);

            title = itemView.findViewById(R.id.groupTitle);
            container = itemView.findViewById(R.id.populateView);

            title.setText(groupMatch.getTitle());
            container.setVisibility(View.GONE);
            title.setOnClickListener(v -> {
                if (container.getVisibility() == View.VISIBLE)
                    container.setVisibility(View.GONE);
                else
                    container.setVisibility(View.VISIBLE);
            });

            for (int j = 0; j < groupMatch.getModelList().size(); j++) {

                MatchModel matchModel = groupMatch.getModelList().get(j);

                AppCompatTextView home, away, middleText, topText, bottomText, betBtn;
                AppCompatImageView shareBtn, homeFlag, awayFlag;

                View view = LayoutInflater.from(this.activityReference.get()).inflate(R.layout.match_card, (ViewGroup) itemView, false);

                home = view.findViewById(R.id.homeTeam);
                away = view.findViewById(R.id.awayTeam);
                topText = view.findViewById(R.id.topTextV);
                middleText = view.findViewById(R.id.middleTextV);
                bottomText = view.findViewById(R.id.bottomTextV);
                betBtn = view.findViewById(R.id.betNowBtn);
                shareBtn = view.findViewById(R.id.shareBtn);
                homeFlag = view.findViewById(R.id.homeFlag);
                awayFlag = view.findViewById(R.id.awayFlag);

                if (CampaignModel.isIsTarget() && CampaignModel.isFetchAds()) {
                    betBtn.setVisibility(View.VISIBLE);
                    betBtn.setOnClickListener(v -> this.activityReference.get().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(CampaignModel.getREDIRECT()))));
                }
                else
                    betBtn.setVisibility(View.GONE);

                home.setText(matchModel.getHome());
                away.setText(matchModel.getAway());

                home.setSelected(true);
                away.setSelected(true);

                LocalDateTime localDateTime;

                if (matchModel.getHomeScore().isEmpty() && matchModel.getAwayScore().isEmpty()){
                    betBtn.setAlpha(1f);
                    betBtn.setEnabled(true);
                    localDateTime = LocalDateTime.parse(matchModel.getStartDate(), DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm"));
                    String date = localDateTime.format(DateTimeFormatter.ofPattern("MM/dd/yy"));
                    String time = localDateTime.format(DateTimeFormatter.ofPattern("HH:mm a"));
                    topText.setText(time);
                    bottomText.setText(date);
                    middleText.setText(null);
                }
                else{
                    betBtn.setAlpha(0.3f);
                    betBtn.setEnabled(false);
                    localDateTime = LocalDateTime.parse(matchModel.getEndDate(), DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm"));
                    String date = localDateTime.format(DateTimeFormatter.ofPattern("MM/dd/yy"));
                    topText.setText(matchModel.getHomeScore());
                    bottomText.setText(matchModel.getAwayScore());
                    middleText.setText(date);
                }

                new Handler(Looper.getMainLooper()).post(() -> {
                    Glide.with(this.activityReference.get())
                            .asDrawable()
                            .load(matchModel.getHomeFlag())
                            .error(R.drawable.ic_flag)
                            .into(homeFlag);

                    Glide.with(this.activityReference.get())
                            .load(matchModel.getAwayFlag())
                            .error(R.drawable.ic_flag)
                            .into(awayFlag);
                });

                shareBtn.setOnClickListener(v -> {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        if (ContextCompat.checkSelfPermission(this.activityReference.get(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this.activityReference.get(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
                            ShareToSocial.saveAndShare(matchModel.getHome() + " vs " + matchModel.getAway(), itemView);
                        else
                            this.activityReference.get().requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
                    }
                    else
                        ShareToSocial.saveAndShare(matchModel.getHome() + " vs " + matchModel.getAway(), itemView);
                });

                new Handler(Looper.getMainLooper()).post(() -> container.addView(view));

            }

            new Handler(Looper.getMainLooper()).post(() -> linearLayoutCompat.addView(itemView));

        }

        return null;
    }

    @Override
    public void setDataAfterLoading(Object result) {


    }

}

如您所见,我在第二个 for 循环中为每个页眉布局添加视图,然后在第一个 for 循环末尾将每个页眉布局添加到主布局。

如果您还不熟悉 Java 的 Concurrent util,您也可以使用相同的方法使用旧的 AsyncTask。