尽管使用了 NTP 偏移量,但设备启动指令秒后



背景:

我有两个物理设备,一个Galaxy S3(手机(和一个华硕700T(平板电脑(,我想同时执行同一组指令。因此,我正在使用Android的平台框架基础SNTP客户端代码来实例化SNTP客户端,该客户端获取原子时间,根据系统时间计算偏移量,并将正/负偏移量添加到指令执行时间戳中,以便它在所有设备上以完全相同的时间(在几毫秒内(运行。我正在以一秒钟的间隔进行一组相机手电筒开/关,从整个值开始,例如 12:47:00.000 pm,因为查看我的过程是否正确很明显且相对简单。

问题:

一个设备往往落后于另一个设备(使用秒表非常明显的 3-5 秒(。

案例示例:S3 ~.640 秒落后于原子时,700T ~1.100 秒落后于原子时;700T 明显在 S3 之后开始 ~3.7 秒。

用于解决问题的方法:

有一个Android应用程序ClockSync,它将设备设置为原子时间,并声称精度在20ms以内。在运行我的应用程序之前,我已经将我计算的偏移量与其右边进行了比较,并且它的偏移量和我的偏移量之间的差异不超过~20毫秒(即Clocksync的偏移量可能是.620,我的偏移量在S3或700T上都不会超过.640(。

我在闪光手电筒模式关闭/打开后立即生成时间戳,并且检查出来,设备之间的唯一区别是一个可能领先另一个,因为它是打印系统时间,一个设备可能比另一个慢半秒。

*请注意,大部分NTP偏移已被过滤掉,因为它们的数量之多降低了可读性

根据我手头的物理秒表,S3 明显首先启动,700T 启动后约 2.130 秒。

700吨:

运行我的应用程序之前根据 Clocksync 应用程序偏移:1264

D/NTP Offset﹕ 1254
D/NTP Offset﹕ 1242
D/NTP Offset﹕ 1203
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:1.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:2.203
D/Flash﹕ Flash torch mode on call hit at 2014-08-15 15:17:02.217
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:2.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:3.245
D/dalvikvm﹕ GC_CONCURRENT freed 399K, 13% free 3930K/4496K, paused 14ms+1ms, total 46ms
D/Flash﹕ Flash torch mode off call hit at 2014-08-15 15:17:03.253
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:3.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:4.231
D/Flash﹕ Flash torch mode on call hit at 2014-08-15 15:17:04.236
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:4.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:5.248
D/Flash﹕ Flash torch mode off call hit at 2014-08-15 15:17:05.254
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:5.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:6.237
D/Flash﹕ Flash torch mode on call hit at 2014-08-15 15:17:06.242
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:6.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:7.243
D/Flash﹕ Flash torch mode off call hit at 2014-08-15 15:17:07.255
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:7.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:8.240
D/Flash﹕ Flash torch mode on call hit at 2014-08-15 15:17:08.246
D/dalvikvm﹕ GC_FOR_ALLOC freed 366K, 15% free 3910K/4552K, paused 28ms, total 28ms
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:8.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:9.221
D/Flash﹕ Flash torch mode off call hit at 2014-08-15 15:17:09.227
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:9.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:10.245
D/Flash﹕ Flash torch mode on call hit at 2014-08-15 15:17:10.251

S3:

运行我的应用程序之前根据 Clocksync 应用程序偏移:1141

D/NTP Offset﹕ 1136
D/NTP Offset﹕ 1136
D/NTP Offset﹕ 1137
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:1.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:2.137
D/Flash﹕ Flash torch mode on call hit at 2014-08-15 15:17:02.156
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:2.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:3.135
D/Flash﹕ Flash torch mode off call hit at 2014-08-15 15:17:03.145
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:3.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:4.134
D/Flash﹕ Flash torch mode on call hit at 2014-08-15 15:17:04.143
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:4.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:5.135
D/Flash﹕ Flash torch mode off call hit at 2014-08-15 15:17:05.144
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:5.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:6.133
D/Flash﹕ Flash torch mode on call hit at 2014-08-15 15:17:06.141
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:6.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:7.135
D/Flash﹕ Flash torch mode off call hit at 2014-08-15 15:17:07.145
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:7.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:8.133
D/Flash﹕ Flash torch mode on call hit at 2014-08-15 15:17:08.142
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:8.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:9.136
D/Flash﹕ Flash torch mode off call hit at 2014-08-15 15:17:09.146
D/instrCal before NTPOffset﹕ 2014-8-15 15:17:9.0
D/instrCal after NTPOffset﹕ 2014-8-15 15:17:10.136
D/Flash﹕ Flash torch mode on call hit at 2014-08-15 15:17:10.146

根据标记,每个设备打开/关闭闪光灯所需的时间不超过30毫秒,因此虽然不希望在需要时晚30毫秒,但差异并不大,也无法解释在设备上启动之间的巨大差异。

法典:

一开始,我在活动生命周期方法之外声明了一堆全局变量,例如:

PowerManager.WakeLock wakeLock;
private Camera camera;
private boolean isFlashOn;
private boolean hasFlash;
private SQLiteDbAdapter dbHelper;
private SimpleCursorAdapter dataAdapter;
private Handler instrHandler = new Handler();
private int arrayCounter = 0;
private long NTPOffset;
private Calendar NTPcal = Calendar.getInstance();

启动方法

   @Override
    protected void onStart() {
        super.onStart();
        // Needed to ensure CPU keeps running even though user might not touch screen
        PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
        wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
                "Show wakelook");
        wakeLock.acquire();
        new GetNTPServerTimeTask().execute();

        // On starting the app get the camera params
        getCamera();
        // Get ready to pull instructions from SQLite DB
        dbHelper = new SQLiteDbAdapter(this);
        dbHelper.open();
        // Fetch instructions to be used
        final List<DynamoDBManager.EventInstruction> instructionSet = setListFromInstructionQuery();
        final Runnable runnableInstructions = new Runnable() {
            @Override
            public void run() {
                Log.d("top of runnableInstructions timestamp for instruction #" + arrayCounter, getCurrentTimeStamp());
                String instrType = instructionSet.get(arrayCounter).getInstructionType();
                String instrDetail = instructionSet.get(arrayCounter).getInstructionDetail();
                if (instrType.equals("flash")) {
                    if (instrDetail.equals("on")) {
                        turnOnFlash();
                    } else if (instrDetail.equals("off")) {
                        turnOffFlash();
                    }
                }
                // Get the next instruction time
                arrayCounter++;
                // Loop until we're out of instructions
                if (arrayCounter < instructionSet.size()) {
                    String startTime = instructionSet.get(arrayCounter).getInstructionStartTime();
                    Calendar instrCal = convertISO8601StringToCal(startTime);
                    printYMDHMSM("instrCal before NTPOffset", instrCal);
                    instrCal.add(Calendar.MILLISECOND, (int) NTPOffset);
                    printYMDHMSM("instrCal after NTPOffset", instrCal);
                    long diff = instrCal.getTimeInMillis() - System.currentTimeMillis();
                    String sDiff = String.valueOf(diff);
                    Log.d("Timestamp at difference calculation", getCurrentTimeStamp());
                    Log.d("Difference", "Difference " + sDiff);
                    instrHandler.postDelayed(this, diff);
                }
            }
        };
        Runnable runnableInstructionsDelay = new Runnable() {
            @Override
            public void run() {
                Log.d("Timestamp at get first instruction time", getCurrentTimeStamp());
                String startTime = instructionSet.get(arrayCounter).getInstructionStartTime();
                Calendar instrCal = convertISO8601StringToCal(startTime);
                printYMDHMSM("First instr instrCal before NTPOffset", instrCal);
                instrCal.add(Calendar.MILLISECOND, (int) NTPOffset);
                printYMDHMSM("First instr instrCal after NTPOffset", instrCal);
                long diff = instrCal.getTimeInMillis() - System.currentTimeMillis();
                instrHandler.postDelayed(runnableInstructions, diff);
            }
        };
        // Get the first instruction time
        if (arrayCounter < instructionSet.size() && arrayCounter == 0) {
            // Since activity gets auto-switched to 30 seconds before first instruction timestamp we want to 
            // use only the most recent NTP offset right before launching the instruction set
            instrHandler.postDelayed(runnableInstructionsDelay, 25000);
        }
    }

循环并设置全局 NTP 偏移变量的 NTP 偏移异步任务

public class GetNTPServerTimeTask extends
            AsyncTask<Void, Void, Void> {
        long NTPnow = 0;
        @Override
        protected Void doInBackground(Void... voids
        ) {
            SntpClient client = new SntpClient();
            if (client.requestTime("0.north-america.pool.ntp.org", 10000)) {
                NTPnow = client.getNtpTime() + SystemClock.elapsedRealtime() - client.getNtpTimeReference();
                NTPcal.setTime(new Date(NTPnow));
                // If NTPCal is ahead, we want the value to be positive so we can add value to system clock to match
                NTPOffset = NTPcal.getTimeInMillis() - System.currentTimeMillis();
                // Time debugging
                Log.d("NTP Now", String.valueOf(NTPnow));
                Log.d("NTP SystemTime", String.valueOf(System.currentTimeMillis()));
                Log.d("NTP Offset", String.valueOf(NTPOffset));
                printYMDHMSM("Calendar Instance", Calendar.getInstance());
                printYMDHMSM("NTPCal Value", NTPcal);
            }
            return null;
        }
    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        new GetNTPServerTimeTask().execute();
    }
}

闪光灯开/关方法:

private void turnOnFlash() {
    if (!isFlashOn) {
        if (camera == null || params == null) {
            return;
        }
        params = camera.getParameters();
        params.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
        Log.d("Flash", "Flash torch mode on call hit at " + getCurrentTimeStamp());
        camera.setParameters(params);
        camera.startPreview();
        isFlashOn = true;
    }
}
private void turnOffFlash() {
    if (isFlashOn) {
        if (camera == null || params == null) {
            return;
        }
        params = camera.getParameters();
        params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
        Log.d("Flash", "Flash torch mode off call hit at " + getCurrentTimeStamp());
        camera.setParameters(params);
        camera.stopPreview();
        isFlashOn = false;
    }
}

我写的时间戳方法:

public static String getCurrentTimeStamp() {
    try {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        String currentTimeStamp = dateFormat.format(new Date()); // Find todays date
        return currentTimeStamp;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

你说你使用相机闪光灯只是为了测试你的方法是否有效,但我认为你选择的测试用例是让你陷入麻烦的原因。除非你的最终目标是让相机同时闪光,否则请尝试选择不同的东西来测试它。你可以让它们播放声音,但音频子系统中可能存在一些不可预测的延迟——更好的测试是你有一个更明确的控制,比如通过UI框架在屏幕上闪烁一些东西,或者更好的是,通过GLSurfaceView在屏幕上闪烁一些东西,在那里你可以非常细粒度地控制帧速率,并确切地知道延迟应该是多少。

我认为这里发生的事情是,你有来自两个不同供应商的两个完全不同的设备。我不确定,但我猜三星有一个三星提供的相机实现,它可能会针对低启动延迟进行优化,所以你可以从口袋里掏出手机,非常快速地拍照。华硕做出了不同的权衡(它是平板电脑,摄影不太重要(。这些设备当然也使用不同的相机硬件并具有不同的驱动程序。因此,即使您在两台设备上几乎同时调用相机子系统,它们实际上对该呼叫的响应也不同。它可能必须启动另一个进程,发送一个意图,让实时摄像机预览,出去泡杯咖啡,诸如此类。或者,如果您在运行相同操作系统的两个相同设备的情况下运行测试,您可能会获得更好的结果。

作为更笼统的评论,我不知道你的总体目标是什么,但不要抱有希望在太严格的容忍范围内实现同时性——很多事情都对你不利。Android并不是作为实时操作系统设计的,即使在延迟很重要的地方,如实时音频,它也有一些工作要做。操作系统可能不会给你你想要的进程调度延迟,Android上的Java可能有点不可预测,除非你非常小心内存分配等(垃圾回收在错误的时间,一切都在窗口之外(。 如果发生其他事情,您的流程随时可能会受到干扰。即使是一开始的NTP同步也可能有点令人担忧,特别是如果您通过移动网络连接而不是WiFi进行同步(尽管话虽如此,我不知道协议在处理这个问题方面有多好(。让事情在半秒内应该是可行的,也许,我认为,让事情低于 10 毫秒可能是非常困难的,介于两者之间的某个地方将是......介于两者之间。

更新 1

您正在使用android.os.Handler类来计时。我没有花时间梳理你的日志,看看当两个设备都被唤醒并试图向外界发出信号时,你有多接近同时,但 Handler 可能做得不是很好。如果部分问题在于设备甚至不认为它们彼此非常接近,通过其内部时钟+ NTP时间戳来衡量,那么您可以尝试其他方法,例如android.app.AlarmManager 。不知道这会更好还是更糟,但它会有所不同。

最新更新