【问题标题】:Android how to avoid i/o problems when periodically writing to a file on internal memoryAndroid如何在定期写入内部存储器上的文件时避免i/o问题
【发布时间】:2013-12-06 11:28:20
【问题描述】:

我正在编写一个应用程序,它将各种传感器(一个位置)和特定于设备的元数据记录到文件中,然后将文件传输到服务器。它不是后台服务 - 应用程序只需要在应用程序处于活动状态时写入和传输文件(无需设置警报来唤醒服务)。每次调用onLocationChanged() 时,我都想在文件中写入一行(尽管位置数据不是唯一被写入的数据)——或者至少以与调用onLocationChanged() 相似的速率。 onLocationChanged() 目前每秒调用一次,但我们最终可能会以更高的速率(可能 2-3x/秒)记录数据。这似乎是对内部存储器的相当数量的 i/o。

我目前一切正常(概念证明),但我需要改进用于写入文件的方法。每次调用onLocationChanged() 时,我都会在文件中写入一行,这可能不明智,而且似乎导致 i/o 堆积。我读过其他类似的问题,这些问题涉及各种方法(新线程、警报、计时器任务、处理程序等),但我找不到特定于我正在尝试做的事情的答案。我还考虑了其​​他方法,例如缓存/缓冲数据并且仅以较低频率(每 10 秒一次?)写入内部存储,或者可能写入 SQLite 数据库并稍后导出到文件。我该怎么做才能最好地将文件代码与传感器代码分离(假设这是我需要做的)并确保及时更新文件?我还需要确保所有数据都写入文件。

更新: 我最终使用的解决方案涉及为一组行(每次调用 onLocationChanged() 一行)附加到 StringBuilder。在附加 10 行数据并有效缓冲后,我将 StringBuilder 交给 AsyncTask,在该任务中将数据写入文件。

【问题讨论】:

    标签: java android file-io io


    【解决方案1】:

    在普及定位课程中遇到过类似的情况。

    这是我们所做的:

    文件编写器 写入文件部分对我们来说不是问题,因为我们只收集 GPS 位置而不是其他传感器数据。对我们来说,像下面这样的实现就足够了。不清楚是否必须将数据写入同一个文件,如果没有,那么您应该可以直接使用它。

    public class FileOutputWriter {
    
    
    private static String pathString = "";
        private static String sensorString = "";
    
    public static void setPath(Context context, String path) {
        pathString = path;
        File pathFile = new File(pathString);
        pathFile.mkdirs();
    }
    
    
    public static void writeData(String data, String sensor, boolean append) {
    
        File file = new File(pathString + "/" + sensor+ ".log");
    
        long timeStamp = System.currentTimeMillis();
    
        BufferedWriter out = null;
        try {
            out = new BufferedWriter(new FileWriter(file, append));
                out.write(timeStamp + ":" + data);
    
            out.newLine();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    }
    

    上传者 为了将数据上传到服务器,我们创建了一个 LogEntries 提示(将其更改为您自己的数据持有者对象或简单的字符串)。

    public class DataLogger {
    
    static Vector<LogEntry> log = new Vector<LogEntry>();
    
    
    public static void addEnty(LogEntry entry) {
        Log.d("DEBUG", entry.getStrategy() + " added position to logger " + entry.getLocation());
        log.add(entry);
    }
    
    public static Vector<LogEntry> getLog() {
        return log;
    }
    
    public static void clear() {
        log.clear();
    }
    }
    

    注意 Vector 是线程安全的。

    最后我们实现了一个 UploaderThread,负责定期检查 DataLogger cue 并上传添加的条目。

    public class UploaderThread extends Thread {
    
    
    public static LinkedList<String> serverLog = new LinkedList<String>();
    
    
    Boolean stop = false;
    Context c;
    
    
    
    public UploaderThread(Context c) {
        this.c = c;
    }
    
    public void pleaseStop() {
        stop = true;
    }
    
    @Override
    public void run() {
        while(!stop) {
    
            try {               
                if(DataLogger.log.size() > 0 && Device.isOnline(c)) {
                    while(DataLogger.log.size() > 0) {
    
                        LogEntry logEntry = DataLogger.getLog().get(0);
                        String result = upload(logEntry);
                        serverLog.add("("+DataLogger.log.size()+")"+"ServerResponse: "+result);
    
                        if(result != null && result.equals("OK")) {
                            DataLogger.getLog().remove(0);
                        } else {
                            Thread.sleep(1000);
                        }
                    }
                } else {
                    serverLog.add("Queue size = ("+DataLogger.log.size()+") + deviceIsonline: "+Device.isOnline(c));
                }
    
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    private String upload(LogEntry entry) {
        HttpClient httpclient = new DefaultHttpClient();
        HttpPost httppost = new HttpPost("http://yoururl/commit.php");      
    
        try {
            // Add your data
            List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>();
            nameValuePairs.add(new BasicNameValuePair("tracename",sessionName +"-"+ entry.getStrategy().toString()));
            nameValuePairs.add(new BasicNameValuePair("latitude", Double.toString(entry.getLocation().getLatitude())));
            nameValuePairs.add(new BasicNameValuePair("longitude", Double.toString(entry.getLocation().getLongitude())));
            nameValuePairs.add(new BasicNameValuePair("timestamp", Long.toString(entry.getTimestamp())));
    
            httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));
    
            // Execute HTTP Post Request
            HttpResponse response = httpclient.execute(httppost);
            if(response != null) {
                InputStream in = response.getEntity().getContent();
                String responseContent = inputStreamToString(in);
    
                return responseContent;
            }
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }       
    
    
        return null;
    }
    
    
    private String inputStreamToString(InputStream is) throws IOException {
        String line = "";
        StringBuilder total = new StringBuilder();
    
        // Wrap a BufferedReader around the InputStream
        BufferedReader rd = new BufferedReader(new InputStreamReader(is));
    
        // Read response until the end
        while ((line = rd.readLine()) != null) { 
            total.append(line); 
        }
    
        // Return full string
        return total.toString();
    }
    
    }
    

    线程只是在您应用的第一个活动中启动:

        UploaderThread ut;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        FileOutputWriter.setPath(this, Environment.getExternalStorageDirectory()
                .getAbsolutePath());
    
        ut = new UploaderThread(this);
        ut.start();
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        ut.pleaseStop();
    }
    

    希望这能让你上路

    【讨论】:

    • 感谢@cYrixmorten,这很有帮助,与我正在尝试做的类似。我正在关注你的代码,但如果你不介意,我会问几个问题:你在哪里打电话给FileOutputWriter.writeData() - 你是在你的主要活动中从onLocationChanged() 打电话给这个的吗? DataLogger.addEntry() 也有同样的问题?我可能有点困惑,因为我在onLocationChanged() 中看不到你在做什么。
    • 嗯,你是对的,我没有在我的代码中包含它。这样做的原因是我们有一些不同目的的不同设置。但是,我认为对于您的应用来说,在 onLocationChanged() 中同时执行这两个操作是有意义的。
    • 谢谢 - 我的主要问题与避免写入期间的文件 I/O 问题有关,但您的示例代码有助于思考线程上传过程。
    【解决方案2】:

    建议在单独的线程上执行所有文件 I/O。您可以设置生产者/消费者结构,其中生产者是向队列生成写入请求的 UI 线程,消费者是在写入请求排队时唤醒并更新文件/数据库/其他内容的工作线程。

    除非您对“及时更新”的定义相当严格,否则这将起作用。但是,由于您正在考虑排队 10 秒,我猜这不是问题。

    编辑: 为了处理可能备份的队列,您应该安排消费者在唤醒时处理队列中的所有内容。如果 I/O 根本无法跟上,即使使用这样的批处理,您也可以安排队列在达到最大大小时丢弃元素。您可能还需要降低调用onLocationChanged() 的频率。

    【讨论】:

    • 这似乎没有回答实际问题 - 当然,线程可能是实现的一部分 - 但没有解决“它应该尝试做什么”问题 - 只是移动一个“数据可用”触发的后台线程写入将对 UI 响应之外的任何内容产生最小的影响。
    • 我应该澄清一下 - 如果可以避免这种情况,我宁愿不缓冲/排队写入 10 秒
    • @rsg - 如果数据的生成速度快于 I/O 处理它们的速度,则需要某种缓冲(或者您需要愿意丢弃数据,或两者兼而有之)。 10 秒没有什么神奇之处。据推测,数据的写入速度可能比这更快。但是,您确实需要处理容量不匹配的问题。我在回答中添加了一些想法。
    猜你喜欢
    • 1970-01-01
    • 2017-03-24
    • 1970-01-01
    • 2019-02-09
    • 1970-01-01
    • 2012-06-07
    • 1970-01-01
    • 1970-01-01
    • 2015-09-05
    相关资源
    最近更新 更多