背景
我有两个物理设备,Galaxy S3(手机)和华硕700T(平板电脑),我想在同一时间执行相同的指令集。因此,我使用Android的Platform Frameworks Base SNTP client code来实例化SNTP客户端,该客户端获取原子时间,根据系统时间计算偏移量,并将正/负偏移量添加到指令执行时间戳,以便它在所有设备的完全相同的时间(在几毫秒内)。我正在以一秒的间隔打开/关闭一组相机手电筒,从整个值开始,例如下午12:47:00.000因为它是显而易见的,并且相对简单地看我的过程是否正确。
问题:
一个设备往往远远落后于另一个设备(使用秒表非常明显的3-5秒)。
案例:原子时间S3~640秒,原子时间700T~1.100秒;在S3之后,700T明显开始~3.7秒。
解决问题的方法:
有一个Android应用,ClockSync可将设备设置为原子时间,并声称其准确度在20毫秒内。在运行我的应用程序之前,我已将计算出的偏移量与其右侧进行了比较,并且其偏差与我的偏差相差不超过约20ms(即Clocksync的偏移量可能为.620,在S3或700T上,我的距离不会超过.640) )。
我在闪光灯手电筒模式关闭/开启后立即生成时间戳,并且事情结束,设备之间的唯一区别是,一个可能比另一个稍早,因为它打印系统时间和一个设备可能是关于比另一半慢半秒。
*请注意,由于大量的NTP偏移会降低可读性,因此已将其过滤掉了
根据我手边的物理秒表,S3显然首先启动,700T开始大约2.130秒。700T:
在运行我的应用程序之前根据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();
onStart方法
@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偏移异步任务循环并设置全局NTPoffset变量
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();
}
}
Flash开/关方法:
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;
}
}
答案 0 :(得分:2)
你说你正在使用相机闪光灯作为测试,看看你的方法是否有效,但我认为你选择一个测试用例会让你遇到麻烦。除非你的最终目标是让相机同时闪光,否则请尝试选择不同的东西来测试它。您可以让它们播放声音,但音频子系统中可能存在一些不可预测的延迟 - 更好的测试可能是您有更明确的控制,例如通过UI框架在屏幕上闪烁某些东西,或者更好的是,闪烁通过GLSurfaceView在屏幕上显示某些内容,您可以对帧速率进行非常精细的控制,并准确了解延迟应该是什么。
我认为这里发生的事情是你有两个完全不同的设备来自两个不同的供应商。我不确定,但我猜三星有一个三星提供的相机实现,它可能会针对低启动延迟进行优化,因此您可以将手机拉出你的口袋很快就能拍照。华硕做出了不同的权衡(它是平板电脑,摄影不那么重要)。这些设备当然也使用不同的相机硬件并具有不同的驱动程序因此,即使您在两个设备上几乎同时对相机子系统进行呼叫,它们实际上也会以不同的方式响应该呼叫。它可能需要启动另一个进程,发送一个意图,进行实时摄像头预览,出去做一杯咖啡,那种事情。或者,如果您使用运行相同操作系统的同一设备中的两个运行测试,则可能会获得更好的结果。
作为一个更为一般性的评论,我不知道你的总体目标是什么,但是不要因为能够在过于宽容的范围内实现同时性而抱有希望 - 很多事情都是对你不利Android并不是作为实时操作系统设计的,即使在像现场音频那样重要的延迟的地方,也有一些赶上来做。操作系统可能无法为您提供流程所需的调度延迟,并且Android上的Java可能有点不可预测,除非您非常小心内存分配等(在错误的时间进行垃圾收集,一切都已经过了窗口)。如果发生其他事情,您的流程可能会随时出现。即使您在开始时的NTP同步也可能有点令人担忧,特别是如果您通过移动网络连接进行同步而不是WiFi(虽然已经说过,我不知道协议在处理时有多好那)。让事情在半秒内完成应该是可行的,也许,我认为,将事情变得不到10毫秒可能会非常艰难,介于两者之间的某个地方将介于两者之间。
更新1
您正在使用android.os.Handler
课程来完成时间安排。我没有花时间梳理你的日志,看看当两个设备被唤醒并试图向外界发出信号时,你与它同时接近,但Handler可能做得不是很好工作。如果问题的一部分是设备甚至不认为他们彼此非常接近,以内部时钟+ NTP时间戳来衡量,那么你可以尝试不同的东西,比如android.app.AlarmManager
。不知道这会更好还是更糟,但会有所不同。