【问题标题】:Efficiently Inflating a lot of Views within several Horizontal LinearLayout with Adapters使用适配器在多个水平线性布局中有效地膨胀大量视图
【发布时间】:2015-03-12 19:39:32
【问题描述】:

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

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

我目前实现这一点的方式是利用其中两个 LinearListView。一种是垂直的,数据由水平的 LinearListViews 组成。这通过创建表视图来提供所需的输出。然后,我使用 Verticle ScrollView 将表格包装在 Horizo​​ntal ScrollView 中,以便可以平移表格(向上/向下或向左/向右滚动)。

这种布局的问题是适配器 getView() 仅在视图初始化时调用(第一次膨胀第一个线性列表时,它膨胀每个视图)。一旦每个视图都膨胀了,getView() 方法就不会在滚动列表时再次调用(它当然滚动得很好,因为它已全部加载)。膨胀所需的总时间并不是什么大问题,但它连续膨胀表中的每个项目这一事实锁定了主 UI 线程。我需要视图在不阻塞 UI 线程的情况下“延迟加载”表中的每个项目。

我有一个屏幕截图,但我没有足够的声望来发布它,也没有足够的资源来使用两个链接。我将尝试在 cmets 中放置外部照片链接。 (参考屏幕截图)表数据是在一组异步任务中生成的,其中每个表项是以毫秒为单位的当前时间(我知道由于异步性质,表行没有排序,稍后会修复) .这个实际的应用程序除了演示这个库之外没有其他目的。

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

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

  • UI 需要采用表格格式,其中每行可以有不同的长度并且可以动态更改。可以从任何部分移除和添加项目。
  • 我的 getView() 方法(2 个适配器和 2 个 LinearListViews)正在使用 ViewHolder 模式并且经过了相当优化。
  • 我的主要目标不一定是提高总速度,而是有效地加载每个视图,以便主 UI 线程不会被锁定(我希望在加载表格时仍然能够使用界面)。李>
  • 为简单起见,将每个单元格的内容视为文本视图(稍后可以支持图像和其他复杂组件)。
  • 表格需要能够向任何方向滚动(不可能一次在屏幕上显示其所有内容)。

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

我会继续努力解决这个问题,并将我发现的任何新内容发回。

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

-埃文

【问题讨论】:

  • 这是我在帖子中提到的外部屏幕截图:Screenshot of example。感谢您的帮助!
  • 您可能想看看将RecyclerView 与自定义LayoutManager 结合使用。

标签: android performance android-layout android-linearlayout android-adapter


【解决方案1】:

由于你关心 UI 性能,你可以使用 AsyncTask 抽象类,常用和谷歌推荐。 AsyncTask 在与 UI 不同的线程上运行。要使用它,您必须创建一个类来对其进行子类化。谷歌网页@AsyncTask,为您提供方便。

我找到的代码示例位于Using an AsyncTask to populate a ListView。 在代码中,请注意 getItemLists 扩展了 AsyncTask。重写该类中的 onPostExecute() 调用 setListAdapter 方法,您可能对此很熟悉。

上面链接中的代码 sn-ps:

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,但我可能会。请随时通知我们。祝你好运...

【讨论】:

  • 非常感谢您的想法!你在正确的轨道上! :)
  • @Evan,谢谢你的好消息。就像我说的,我将来可能会这样做。出于某种原因,其他人不喜欢我的回答,现在它的“-1”没有帮助。
  • 是的,我不知道为什么有人这样做。一旦我能够投票,我就会投票给你的答案(还没有 15 个代表)。我当然不同意他们对你的答案投反对票的决定。再次感谢您的帮助!
  • Evan,与此同时,您可以通过选择复选图标来选择我发布的答案作为正确答案。你不需要15个代表。感谢您的评论。
【解决方案2】:

我想通了:)

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

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

/**
 * 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 体验。 :)

享受,

-埃文

【讨论】:

    【解决方案3】:

    在使用手风琴样式布局时,我能够做到这一点,其中每个折叠项也包含列表的一个子集,因为这种布局不适用于 RecyclerView Viewholder 模式。我使用 Concurrent 作为 Asynctask 的替代品,然后在 doInBackground 部分所有 Glide 获取图像和 addView() 调用使用 new Handler(Looper.getMainLooper()).post(() -&gt; {//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。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-07-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多