难以理解 Android 应用程序中的复杂多线程

Difficulty in understanding complex multi threading in Android app

我在理解我的应用程序中的多线程方面遇到了很大的问题,因此发现了一个错误。我已经检查了我认为所有的可能性,但我仍然遇到各种(有时是意外的)错误。

也许这里有人能给我建议,我应该怎么做。

在我的项目中,我使用了两个外部库:

至于应用程序,其结构如下:

           MainActivity
            /        \
           /          \
        Thread        Fragment
   (ProcessThread)   (GraphFragment)

想法是 ProcessThread 计算数据并向 GraphFragmentEventBus 提供恒定的值流。在 GraphFragment 中,我有一个 Series 需要 GraphView

要根据 example 实时更新图表我需要制作一个新的 Runnable 所以我做了一个:

private class PlotsRun implements Runnable{

        @Override
        public void run() {
            mSeries1.appendData(new DataPoint(counter, getRandom()), true, 100);
            counter++;
            mHandler.post(this);
        }
}

当我从片段的 onResume() 方法启动它时,一切都运行良好。

不幸的是,正如我所提到的,我正在使用来自另一个线程的外部数据。为了得到它 GraphFragment 我正在使用(根据 documentationonEventMainThread() 方法。

在这里,无论我做什么,我都无法传递数据来更新 PlotsRun 对象中的图形。到目前为止我已经尝试过:

我很难理解的是这个运行正常的(在某些时候)。

因为它在官方 Android 博客上是 said,所以您无法从非 UI 线程更新 UI。这就是为什么我不能在 GraphFragment 中使用另一个线程的原因。但是当我检查我的 runnable 时,它​​在主线程上是 运行 (UI)。这就是为什么我不能在那里创建无限 while loop 而必须调用 mHandler.post(this).

而且它的行为仍然像另一个线程,因为它比 onEventMainThread 方法更快(调用更频繁)。

我该怎么做才能使用来自 ProcessThread 的数据更新我的图表(或我应该查看的位置)?

编辑 1:

回答@Matt Wolfe 的请求,我将我认为是解决此问题的代码中最重要的部分包含在内,所有必需的变量都显示了它们的声明方式。这是一个非常简单的例子:

MainActivity:

private ProcessThread testThread = new ProcessThread();

 @Override
    protected void onResume() {
        super.onResume();
        testThread.start();
    }


    private class ProcessThread extends Thread{
        private float value = 0f;
        private ReadingsUpdateData updater = new ReadingsUpdateData(values);
        public void run() {
            while(true) {
                value = getRandom();
                updater.setData(value);
                EventBus.getDefault().post(updater);
            }
        }
    }

GraphFragment:

private LineGraphSeries<DataPoint> mSeries1;
    long counter = 0;
    private Queue<ReadingsUpdateData> queue;

    @Override
    public void onResume() {
        super.onResume();
        mTimer2.run();
    }

    public void onEventMainThread(ReadingsUpdateData data){
        synchronized(queue){
            queue.add(data);
        }
    }

    private class PlotsRun implements Runnable{

        @Override
        public void run() {
            if (queue.size()>0) {
                mSeries1.appendData(new DataPoint(counter, queue.poll()), true, 100);
                counter++;
            }
            mHandler.post(this);
        }
    }

因此在runnable中加入if来保护快速读取问题。但它不应该在这里,因为总应该有一些东西(至少我希望如此)。

还有一件事要补充——当我把简单的 Log.d 和计数变量放在 onEventMainThread 中时,它正在更新并正确显示它的值,但不幸的是 logcat 不是主要的 UI.

编辑2:

这主要是对@MattWolfe 的回应

mHandler 只是在 GrapgFragment 中声明和创建的变量:

private final Handler mHandler = new Handler();
private Runnable mTimer2;

是的,没错,我正在使用 mHandler.post(),没有任何延迟。我会尝试使用一些延迟,看看是否有任何不同。

我之前没有提到的是 ProcessThread 还向其他片段提供数据 - 不要担心它们不会相互干扰或共享任何资源。这就是我使用 EventBus.

的原因

编辑3:

这是我在 GraphFragmentrunOnMainThread 方法的另一个线程中用作另一个想法的代码:

private MyThread thread = new MyThread();

    private class MyThread extends Thread {
        Queue<ReadingsUpdateData> inputList;
        ReadingsUpdateData msg;

        public MyThread() {
            inputList = new LinkedList<>();
        }

        public void run() {
            while(true) {
                try{
                    msg = inputList.poll();
                } catch(NoSuchElementException nse){
                    continue;
                }
                if (msg == null) {
                    continue;
                }
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mSeries1.appendData(new DataPoint(counter, getRandom()), true, 100);
                        counter++;
                    }
                });
            }
        }

        public void onEvent(ReadingsUpdateData data){
            inputList.add(data);
        }
    }

不幸的是,它也不起作用。

我会这样设置:

public class MainActivity extends Activity {

 private class ProcessThread extends Thread{
        private float value = 0f;
        private ReadingsUpdateData updater = new ReadingsUpdateData(values);
        public void run() {
            while(true) {
                value = getRandom();
                updater.setData(value);
                EventBus.getDefault().post(updater);
            }
        }
    }    

    @Override
    protected void onResume() {
        super.onResume();
        testThread.start();
    }

}



public class GraphFragment extends Fragment {

  private Handler mHandler;
  private Queue<ReadingsUpdateData> queue;

  @Override
  public void onActivityCreated(Bundle state) {
    super.onActivityCreated(state);
    mHandler = new Handler(Looper.getMainLooper());
  }

  public void onEvent(ReadingsUpdateData data){
    synchronized(queue){
        queue.add(data);
    }

    if (mHandler != null) {
       mHandler.post(processPlots);
    }
  }

  //implement pause/resume to register/unregister from event bus


 private Runnable processPlots = new Runnable {

        @Override
        public void run() {
            synchronized(queue) {
              if (queue.size()>0) {
                mSeries1.appendData(new DataPoint(counter, queue.poll()), true, 100);
                counter++;
              }
            }
        }
    }          

}

首先,

后面示例中的可运行部分只是为了动画实时数据更新,您可以选择调用 appendData() 而无需创建新的可运行部分。不过,您需要从主线程调用 appendData()

其次,

您可以直接从 onEventMainThread 函数调用 appendData() 函数,但正如您指出的那样,这种方法有时会挂起 UI,这种行为的一个可能原因是您可能 post 事件 过于频繁 ,更新 UI 过于频繁最终会在某个时候挂起 UI。您可以执行以下操作来避免这种情况:

更新 UI 过于频繁也可能挂起 UI, 这是一个解决方案:

ProcessThread中添加一些逻辑来保存上次发送的事件时间并在发送新事件之前进行比较,如果差异小于 1 秒则将其保存以供稍后发送和下一次计算完成时使用, 再次比较时间,如果现在大于 1 秒,则发送数组中的事件或者可能只发送最新的事件,因为最新的计算可以代表图形的最新状态对吗?

希望对您有所帮助!

编辑:(回应评论 1 和 2)

我不确定您尝试的是什么 post更新您的代码会给出更好的主意。但我认为您尝试在 onEventMainThreadPlotsRun runnable 中实现时间检查功能,对吗?如果是,那恐怕对您没有太大帮助。相反,您需要做的是在 ProcessThread 内部实施这次检查检查,如果达到阈值时间,则仅 post 新事件。原因如下:

1-后端的EventBus自动创建一个新的runnable并在其中调用onEventMainThread。因此,在 ProcessThread 内处理时间检查会在内存中产生更少不需要的可运行对象,从而减少内存消耗。

2- 也无需维护队列和生成新的可运行对象,只需更新 onEventMainThread.

中的数据

下面是一个最低限度的代码,仅用于提供概念验证,您需要根据需要更新它:

ProcessThread class:

private class ProcessThread extends Thread{
    private static final long TIME_THRESHOLD = 100; //100 MS but can change as desired
    private long lastSentTime = 0;
    private float value = 0f;
    private ReadingsUpdateData updater = new ReadingsUpdateData(values);
    public void run() {
        while(true) {
            if (System.currentTimeMillis() - lastSentTime < TIME_THRESHOLD) {
                try {
                    Thread.sleep(TIME_THRESHOLD - (System.currentTimeMillis() - lastSentTime));
                } catch (InterruptedException e) {}
            }

            value = getRandom();
            updater.setData(value);
            EventBus.getDefault().post(updater);
            lastSentTime = System.currentTimeMillis();
        }
    }
}

onEventMainThread方法:

public void onEventMainThread(ReadingsUpdateData data){
    mSeries1.appendData(new DataPoint(counter, data), true, 100);
    counter++;
}

你的 PlotsRun 实际上太快了:一旦它完成执行,它就会通过调用 mHandler.post(processPlots);.

在主线程循环执行中重新排队。

首先,您需要使数据缓冲区独立于数据收集器和数据可视化器:创建一个可以接收(来自收集器)和传送(至可视化器)数据的对象。因此,每个组件都可以完全独立地工作。并且您的数据对象独立于任何线程。您的数据收集器可以在需要时将数据推送到您的数据对象,您的主线程可以根据常规计时器查询您的数据对象。

然后,锁定这个缓冲区,这样两个其他需要访问数据缓冲区的对象都不能同时访问(这会导致崩溃)。这个锁可以是方法声明中的一个简单的synchronized.

这应该可以确保您的应用不会因为并发访问而崩溃(我认为这应该是您的主要问题)。

然后,如果在新数据到达时主数据集合已在使用中,您可以通过创建额外的缓冲区来存储临时数据来开始优化数据对象,或者制作实际数据的副本以使其始终可用主线程即使在主线程查询值时当前正在添加新数据。

尝试使用可以从您的 Fragment 或 Activity 执行的 AsyncTask。这是 AsyncTask

的 Android 文档的 link
public class SomeAsyncTask extends AsyncTask<Object,Void, Object>{
        @Override
        protected void onPreExecute(){

        }
        @Override
        protected Object doInBackground(Object… params) {
        //make your request for any data here
           return  getData();


        }
        @Override
        protected void onPostExecute(Object object){
        //update your UI elements here
        mSeries1. appendData(object);           
        }
    }