我有一个关于如何提高大型水平线性布局性能的问题。
我正在创建一个类似于视图的表,可以包含50到2,500个条目。每个条目都是一个LinearLayout,其中包含带有一些简单文本的TextView。我使用LinearListView library实现了设计。此库允许将ListAdapter绑定到LinearLayout以显示水平或垂直方向的视图。
我目前实现此方法的方法是使用其中两个LinearListViews。一个是垂直的,数据由水平LinearListViews组成。这通过创建表视图来提供所需的输出。然后,我使用Verticle ScrollView将表格包装在水平ScrollView中,以便可以平移表格(向上/向下或向左/向右滚动)。
此布局的问题在于,仅在视图初始化时调用适配器getView()(第一次膨胀第一个linearList时,它会为每个视图充气)。一旦每个视图都膨胀,在滚动列表时永远不会再调用getView()方法(当然它滚动得非常好,因为它全部被加载)。膨胀所花费的总时间并不是很大,但是它连续膨胀表中每个项目的事实会锁定主UI线程。我需要查看"延迟加载"表中的每个项目都没有阻止UI线程。
我有一个屏幕截图,但我没有足够的声誉发布它,也没有足够的使用两个链接。我将尝试在评论中添加外部照片链接。 (参考屏幕截图)表数据是在一组异步任务中生成的,其中每个表项都是以毫秒为单位的当前时间(我知道由于异步性质而没有对表行进行排序,以后会修复它) 。除了演示此库之外,这个实际的应用程序没有任何用途。
我添加了"随机更改数据"按钮,它将创建一个随机点(int x,int y)并生成一个随机字符串,并用该字符串替换(x,y)处的单元格。这通过调用特定的适配器的getView()方法几乎立即发生。所以访问这个表非常快!同样,它是锁定主UI线程的初始膨胀。
总结一些重要的注释:
我发现这个application (TV SideView)创建了一个相当大的表视图,可以非常好地加载。我最终希望实现与此类似的功能(查看"程序指南"页面以查看实际表格)。它加载了一堆单元格,你仍然可以使用UI(在第一次打开时拖动桌子,你会看到加载的单元格)。
我会继续嘲笑这个并发回我找到的任何新内容。
非常感谢任何建议和帮助!非常感谢你的时间!
-Evan
答案 0 :(得分:0)
由于您关注UI性能,您可以使用AsyncTask抽象类,常用和Google推荐。 AsyncTask在与UI不同的线程上运行。要使用它,您必须创建一个类来对其进行子类化。谷歌网页@ AsyncTask,为了您的方便。
我找到的代码示例位于Using an AsyncTask to populate a ListView。 在代码中,请注意getItemLists扩展了AsyncTask。在该类中覆盖 onPostExecute ()会调用您可能熟悉的 setListAdapter 方法。
以上链接的代码段:
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,但我可能会这样做。请留意我们。祝你好运......
答案 1 :(得分:0)
我已经弄明白了:)
@TheOriginalAndroid答案是一个很好的主意和回应!非常感谢你的时间和帮助。我实际上已经开始实现AsyncTask Manager并在昨天早上完成了它。我通过创建一个名为AsycnGridManager的类来解决这个问题,该类将管理负责绘制视图的asyncTasks组。这是相当多的代码,但我在评论中详细介绍了。这不是实际的代码,而是一个shell,用于显示其工作原理的概述。我没有编辑它所以请不要把它作为钻石。应该从主要活动中的主线程或负责它的片段创建和启动此类。
/**
* 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体验。 :)
享受,
-Evan
答案 2 :(得分:0)
在使用手风琴样式布局时,我能够做到这一点,其中每个折叠项也包含列表的一个子集,因为这种布局不适用于 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。