您好,登錄后才能下訂單哦!
在分析Robotium的運行原理之前,我們有必要先搞清楚Instrumentation的一些相關知識點,因為Robotium就是基于Instrumentation而開發出來的一套自動化測試框架。鑒于之前本人已經轉載和編寫了Instrumentation的一些文章,所以建議大家如果沒有看過的還是翻看下先對Instrumentation有個基本的理解。然后帶著疑問再來看這篇文章看是否能幫上自己。
既然是分析Instrumentation,那么我們必須要先看下Instrumentation 這個類的類圖,直接網上截獲,就不花時間另外去畫了,但請注意網上該圖是比較老的,一些新的注入事件的方法是沒有加進去的,注意紅色部分:
開始分析之前我們要搞清楚Instrumentation的幾點
/* */ public class InstrumentationTestRunner /* */ extends Instrumentation /* */ implements TestSuiteProvider /* */ { ... }從它的類定義我們可以看到它是從我們的Instrumentation類繼承下來的。其實從它的名字我們就大概可以想像到它是扮演什么角色的,參照我們之前對UiAutomator的源碼分析《UIAutomator源碼分析之啟動和運行》,InstrumentationTestRunner扮演的角色類似于當中的UiAutomatorTestRunner類,都是通過解析獲取和建立目標測試用例和測試集然后知道測試的運行。
/* */ public void onCreate(Bundle arguments) /* */ { /* 303 */ super.onCreate(arguments); ... /* 343 */ TestSuiteBuilder testSuiteBuilder = new TestSuiteBuilder(getClass().getName(), getTargetContext().getClassLoader()); /* */ /* */ /* 346 */ if (testSizePredicate != null) { /* 347 */ testSuiteBuilder.addRequirements(new Predicate[] { testSizePredicate }); /* */ } /* 349 */ if (testAnnotationPredicate != null) { /* 350 */ testSuiteBuilder.addRequirements(new Predicate[] { testAnnotationPredicate }); /* */ } /* 352 */ if (testNotAnnotationPredicate != null) { /* 353 */ testSuiteBuilder.addRequirements(new Predicate[] { testNotAnnotationPredicate }); /* */ } /* */ /* 356 */ if (testClassesArg == null) { ... /* */ } else { /* 370 */ parseTestClasses(testClassesArg, testSuiteBuilder); /* */ } /* */ /* 373 */ testSuiteBuilder.addRequirements(getBuilderRequirements()); /* */ /* 375 */ this.mTestRunner = getAndroidTestRunner(); /* 376 */ this.mTestRunner.setContext(getTargetContext()); /* 377 */ this.mTestRunner.setInstrumentation(this); /* 378 */ this.mTestRunner.setSkipExecution(logOnly); /* 379 */ this.mTestRunner.setTest(testSuiteBuilder.build()); /* 380 */ this.mTestCount = this.mTestRunner.getTestCases().size(); /* 381 */ if (this.mSuiteAssignmentMode) { /* 382 */ this.mTestRunner.addTestListener(new SuiteAssignmentPrinter()); /* */ } else { /* 384 */ WatcherResultPrinter resultPrinter = new WatcherResultPrinter(this.mTestCount); /* 385 */ this.mTestRunner.addTestListener(new TestPrinter("TestRunner", false)); /* 386 */ this.mTestRunner.addTestListener(resultPrinter); /* 387 */ this.mTestRunner.setPerformanceResultsWriter(resultPrinter); /* */ } /* 389 */ start(); /* */ }從中我們可以看到這個方法開始就是如上面所說的類似UiAutomatorTestRunner一樣去獲取解析對應測試包里面的測試集和測試用例,這個在這個章節不是重點,重點是最后面的start()這個方法的調用。這個方法最終調用的是父類Instrumentation的start()方法,我們看下這個方法的官方解析"Create and start a new thread in which to run instrumentation.“翻譯過來就是”創建一個新的運行Instrumentation(測試用例)的線程":
/* */ public void start() /* */ { /* 122 */ if (this.mRunner != null) { /* 123 */ throw new RuntimeException("Instrumentation already started"); /* */ } /* 125 */ this.mRunner = new InstrumentationThread("Instr: " + getClass().getName()); /* 126 */ this.mRunner.start(); /* */ }在第125行我們很明顯知道新的線程名就叫做"Instr:android.test.InstrumentationTestRunner",因為這個方法是從子類android.test.InstrumentationTestRunner中傳進來的,所以getClass().getName()方法獲得的就是子類的名字。
/* */ private final class InstrumentationThread /* */ extends Thread { /* 1689 */ public InstrumentationThread(String name) { super(); } /* */ /* */ public void run() { /* */ try { /* 1693 */ Process.setThreadPriority(-8); /* */ } catch (RuntimeException e) { /* 1695 */ Log.w("Instrumentation", "Exception setting priority of instrumentation thread " + Process.myTid(), e); /* */ } /* */ /* 1698 */ if (Instrumentation.this.mAutomaticPerformanceSnapshots) { /* 1699 */ Instrumentation.this.startPerformanceSnapshot(); /* */ } /* 1701 */ Instrumentation.this.onStart(); /* */ } /* */ }
/** * Initialize the current thread as a looper. * <p/> * Exposed for unit testing. */ void prepareLooper() { Looper.prepare(); } @Override public void onStart() { prepareLooper(); if (mJustCount) { mResults.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID); mResults.putInt(REPORT_KEY_NUM_TOTAL, mTestCount); finish(Activity.RESULT_OK, mResults); } else { if (mDebug) { Debug.waitForDebugger(); } ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); PrintStream writer = new PrintStream(byteArrayOutputStream); try { StringResultPrinter resultPrinter = new StringResultPrinter(writer); mTestRunner.addTestListener(resultPrinter); long startTime = System.currentTimeMillis(); mTestRunner.runTest(); long runTime = System.currentTimeMillis() - startTime; resultPrinter.printResult(mTestRunner.getTestResult(), runTime); } catch (Throwable t) { // catch all exceptions so a more verbose error message can be outputted writer.println(String.format("Test run aborted due to unexpected exception: %s", t.getMessage())); t.printStackTrace(writer); } finally { mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT, String.format("\nTest results for %s=%s", mTestRunner.getTestClassName(), byteArrayOutputStream.toString())); if (mCoverage) { generateCoverageReport(); } writer.close(); finish(Activity.RESULT_OK, mResults); } } }該方法一開始就為InstrumentationTestRunner線程建立一個looper消息隊列,至于looper是怎么回事,大家如果不清的請查看網絡的解析。Looper是用于給一個線程添加一個消息隊列(MessageQueue),并且循環等待,當有消息時會喚起線程來處理消息的一個工具,直到線程結束為止。通常情況下不會用到Looper,因為對于Activity,Service等系統組件,Frameworks已經為我們初始化好了線程(俗稱的UI線程或主線程),在其內含有一個Looper,和由Looper創建的消息隊列,所以主線程會一直運行,處理用戶事件,直到某些事件(BACK)退出。
public void runTest(TestResult testResult) { mTestResult = testResult; for (TestListener testListener : mTestListeners) { mTestResult.addListener(testListener); } Context testContext = mInstrumentation == null ? mContext : mInstrumentation.getContext(); for (TestCase testCase : mTestCases) { setContextIfAndroidTestCase(testCase, mContext, testContext); setInstrumentationIfInstrumentationTestCase(testCase, mInstrumentation); setPerformanceWriterIfPerformanceCollectorTestCase(testCase, mPerfWriter); testCase.run(mTestResult); } }大概做法就是對所有收集到的測試集進行一個for循環然后取出每個測試用例在junit.Framework.Testcase環境下進行運行了。這里就不往下研究junit框架是怎么回事了。
/** * Runs the specified action on the UI thread. If the current thread is the UI * thread, then the action is executed immediately. If the current thread is * not the UI thread, the action is posted to the event queue of the UI thread. * * @param action the action to run on the UI thread */ public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action); } else { action.run(); } }其代碼的功能和對應的描述一致:
/* */ public void runOnMainSync(Runnable runner) /* */ { /* 344 */ validateNotAppThread(); /* 345 */ SyncRunnable sr = new SyncRunnable(runner); /* 346 */ this.mThread.getHandler().post(sr); /* 347 */ sr.waitForComplete(); /* */ }這里也是從再從主線程獲得Main Looper的Handler后往Main Looper消息隊列中提交action,但人家提交完之后還會等待該action線程的執行完畢才會退出這個函數,所以兩個方法的區別就是:Activity的runOnUiThread是異步執行的,Instrumentation的runOnMainSync是同步執行的。runOnMainSync又是怎么實現這一點的呢?這個我們就要看Instrumetnation的內部類SyncRunnable了:
/* */ private static final class SyncRunnable implements Runnable { /* */ private final Runnable mTarget; /* */ private boolean mComplete; /* */ /* 1715 */ public SyncRunnable(Runnable target) { this.mTarget = target; } /* */ /* */ public void run() /* */ { /* 1719 */ this.mTarget.run(); /* 1720 */ synchronized (this) { /* 1721 */ this.mComplete = true; /* 1722 */ notifyAll(); /* */ } /* */ } /* */ /* */ public void waitForComplete() { /* 1727 */ synchronized (this) { /* 1728 */ while (!this.mComplete) { /* */ try { /* 1730 */ wait(); /* */ } /* */ catch (InterruptedException e) {} /* */ } /* */ } /* */ } /* */ }它也是從runnable線程類繼承下來的。在run方法的1720到1722行我們可以看到,該運行在Main UiThread的方法在跑完后會把Instrumentation實例的mComplete變量設置成true,而runOnMainSync最后調用的運行在子線程中的waitForComplete方法會一直等待這個mComplete變量變成true才會返回,也就是說一直等待主線程的調用完成才會返回,那么到了這里就很清楚runOnMainSync是如何通過SyncRunnable這個內部類實現同步的了。
/* */ private final void validateNotAppThread() /* */ { /* 1650 */ if (Looper.myLooper() == Looper.getMainLooper()) { /* 1651 */ throw new RuntimeException("This method can not be called from the main application thread"); /* */ } /* */ }
Method | Description | Comment |
Key Events | ||
sendKeySync | 發送一個鍵盤事件,注意同一時間只有一個action,或者是按下,或者是彈起,所有下面其他key相關的事件注入都是以這個方法為基礎的 | |
sendKeyDownUpSync | 基于sendKeySync發送一個按鍵的按下和彈起兩個事件 | |
sendCharacterSync | 發送鍵盤上的一個字符,完整的過程包括一個按下和彈起事件 | |
sendStringSync | 往應用發送一串字符串 | |
Tackball Event | ||
sendTrackballEventSync | 發送軌跡球事件。個人沒有用過,應該是像黑莓的那種軌跡球吧 | |
Pointer Event | ||
sendPointerSync | 發送點擊事件 | |
/** * Send a key event to the currently focused window/view and wait for it to * be processed. Finished at some point after the recipient has returned * from its event processing, though it may <em>not</em> have completely * finished reacting from the event -- for example, if it needs to update * its display as a result, it may still be in the process of doing that. * * @param event The event to send to the current focus. */ public void sendKeySync(KeyEvent event) { validateNotAppThread(); long downTime = event.getDownTime(); long eventTime = event.getEventTime(); int action = event.getAction(); int code = event.getKeyCode(); int repeatCount = event.getRepeatCount(); int metaState = event.getMetaState(); int deviceId = event.getDeviceId(); int scancode = event.getScanCode(); int source = event.getSource(); int flags = event.getFlags(); if (source == InputDevice.SOURCE_UNKNOWN) { source = InputDevice.SOURCE_KEYBOARD; } if (eventTime == 0) { eventTime = SystemClock.uptimeMillis(); } if (downTime == 0) { downTime = eventTime; } KeyEvent newEvent = new KeyEvent(downTime, eventTime, action, code, repeatCount, metaState, deviceId, scancode, flags | KeyEvent.FLAG_FROM_SYSTEM, source); InputManager.getInstance().injectInputEvent(newEvent, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH); }這個就很明顯了,用的就是InputManager的事件注入方式,如果大家不清楚的請查看本人之前翻譯的《Monkey源碼分析番外篇之Android注入事件的三種方法比較》。
/** * Dispatch a trackball event. Finished at some point after the recipient has * returned from its event processing, though it may <em>not</em> have * completely finished reacting from the event -- for example, if it needs * to update its display as a result, it may still be in the process of * doing that. * * @param event A motion event describing the trackball action. (As noted in * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use * {@link SystemClock#uptimeMillis()} as the timebase. */ public void sendTrackballEventSync(MotionEvent event) { validateNotAppThread(); if ((event.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) == 0) { event.setSource(InputDevice.SOURCE_TRACKBALL); } InputManager.getInstance().injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH); }最后我們看下點擊事件,同樣,使用的也是無一例外的InputManager的事件注入方式:
/** * Dispatch a pointer event. Finished at some point after the recipient has * returned from its event processing, though it may <em>not</em> have * completely finished reacting from the event -- for example, if it needs * to update its display as a result, it may still be in the process of * doing that. * * @param event A motion event describing the pointer action. (As noted in * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use * {@link SystemClock#uptimeMillis()} as the timebase. */ public void sendPointerSync(MotionEvent event) { validateNotAppThread(); if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0) { event.setSource(InputDevice.SOURCE_TOUCHSCREEN); } InputManager.getInstance().injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH); }
Method | Control by User(Instrumentation) | Control by OS | Comment |
onCreate | callActivityOnCreate | onCreate | |
onDestroy | callActivityOnDestroy | onDestroy | |
onStart | callActivityOnStart | onStarty | |
… | | | |
/** * Perform calling of an activity's {@link Activity#onCreate} * method. The default implementation simply calls through to that method. * * @param activity The activity being created. * @param icicle The previously frozen state (or null) to pass through to * onCreate(). */ public void callActivityOnCreate(Activity activity, Bundle icicle) { ... activity.performCreate(icicle); ... }從代碼可以看到它做的事情也就是直接調用Activity類的performCreate方法:
final void performCreate(Bundle icicle) { onCreate(icicle); mVisibleFromClient = !mWindow.getWindowStyle().getBoolean( com.android.internal.R.styleable.Window_windowNoDisplay, false);而performCreate方法最終調用的就是onCreate方法。注意performCreate這個方法是屬于Internal API,它不是public出去給外部使用的.
/** * Gets the {@link UiAutomation} instance. * <p> * <strong>Note:</strong> The APIs exposed via the returned {@link UiAutomation} * work across application boundaries while the APIs exposed by the instrumentation * do not. For example, {@link Instrumentation#sendPointerSync(MotionEvent)} will * not allow you to inject the event in an app different from the instrumentation * target, while {@link UiAutomation#injectInputEvent(android.view.InputEvent, boolean)} * will work regardless of the current application. * </p> * <p> * A typical test case should be using either the {@link UiAutomation} or * {@link Instrumentation} APIs. Using both APIs at the same time is not * a mistake by itself but a client has to be aware of the APIs limitations. * </p> * @return The UI automation instance. * * @see UiAutomation */ public UiAutomation getUiAutomation() { if (mUiAutomationConnection != null) { if (mUiAutomation == null) { mUiAutomation = new UiAutomation(getTargetContext().getMainLooper(), mUiAutomationConnection); mUiAutomation.connect(); } return mUiAutomation; } return null; }關于UiAutomation更多的描述請查看本人上一個系列關于UiAutomator源碼分析的文章,這里列出來方便大家瀏覽:
/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package come.example.android.notepad.test; import android.test.ActivityInstrumentationTestCase2; import com.example.android.notepad.NotesList; import com.example.android.notepad.NoteEditor; import com.example.android.notepad.NotesList; import com.example.android.notepad.R; import android.app.Activity; import android.app.Instrumentation; import android.app.Instrumentation.ActivityMonitor; import android.content.Intent; import android.os.SystemClock; import android.test.InstrumentationTestCase; import android.view.KeyEvent; import android.widget.TextView; /** * Make sure that the main launcher activity opens up properly, which will be * verified by {@link #testActivityTestCaseSetUpProperly}. */ public class NotePadTest extends ActivityInstrumentationTestCase2<NotesList> { NotesList mActivity = null; /** * Creates an {@link ActivityInstrumentationTestCase2} for the {@link NotesList} activity. */ public NotePadTest() { super(NotesList.class); } //private static Instrumentation instrumentation = new Instrumentation(); @Override protected void setUp() throws Exception { super.setUp(); //Start the NotesList activity by instrument Intent intent = new Intent(); intent.setClassName("com.example.android.notepad", NotesList.class.getName()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Instrumentation inst = getInstrumentation(); mActivity = (NotesList) inst.startActivitySync(intent); } @Override protected void tearDown() { mActivity.finish(); try { super.tearDown(); } catch (Exception e) { e.printStackTrace(); } } /** * Verifies that the activity under test can be launched. */ /* public void testActivityTestCaseSetUpProperly() { assertNotNull("activity should be launched successfully", getActivity()); } */ public void testActivity() throws Exception { //Add activity monitor to check whether the NoteEditor activity's ready ActivityMonitor am = getInstrumentation().addMonitor(NoteEditor.class.getName(), null, false); //Evoke the system menu and press on the menu entry "Add note"; getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_MENU); getInstrumentation().invokeMenuActionSync(mActivity, R.id.menu_add, 0); //Direct to the NoteEditor activity Activity noteEditorActivity = getInstrumentation().waitForMonitorWithTimeout(am, 60000); assertEquals(NoteEditor.class,noteEditorActivity.getClass()); SystemClock.sleep(3000); //assertEquals(true, getInstrumentation().checkMonitorHit(am, 1)); TextView noteEditor = (TextView) noteEditorActivity.findViewById(R.id.note); //Get the text directly, DON'T need to runOnMainSync at all!!! String text = noteEditor.getText().toString(); assertEquals(text,""); //runOnMainSync to change the text getInstrumentation().runOnMainSync(new PerformSetText(noteEditor,"Note1")); //inject events to change the text getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_1); getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_2); getInstrumentation().sendCharacterSync(KeyEvent.KEYCODE_P); getInstrumentation().sendStringSync("gotohell"); //getInstrumentation().callActivityOnPause(noteEditorActivity); Thread.sleep(5000); //getInstrumentation().callActivityOnResume(noteEditorActivity); //Save the new created note getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_MENU); getInstrumentation().invokeMenuActionSync(noteEditorActivity, R.id.menu_save, 0); } private class PerformSetText implements Runnable { TextView tv; String txt; public PerformSetText(TextView t,String text) { tv = t; txt = text; } public void run() { tv.setText(txt); } } }
<table cellspacing="0" cellpadding="0" width="539" class=" " style="margin: 0px 0px 10px; padding: 0px; border-collapse: collapse; width: 668px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;"><tbody style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;"><tr style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;"><td valign="top" width="112" height="39" style="border-style: solid; border-color: rgb(0, 0, 0); margin: 0px; padding: 4px; word-break: break-all; max-width: 100%; word-wrap: break-word !important; box-sizing: border-box !important;"> </td></tr><tr style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;"><td valign="top" width="111" height="13" style="border-style: solid; border-color: rgb(0, 0, 0); margin: 0px; padding: 4px; word-break: break-all; max-width: 100%; word-wrap: break-word !important; box-sizing: border-box !important; background-color: rgb(190, 192, 191);"><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; max-width: 100%; clear: both; min-height: 1em; white-space: pre-wrap; box-sizing: border-box !important; word-wrap: break-word !important;"><span style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;">作者</span></p></td><td valign="top" width="112" height="13" style="border-style: solid; border-color: rgb(0, 0, 0); margin: 0px; padding: 4px; word-break: break-all; max-width: 100%; word-wrap: break-word !important; box-sizing: border-box !important; background-color: rgb(190, 192, 191);"><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; max-width: 100%; clear: both; min-height: 1em; white-space: pre-wrap; box-sizing: border-box !important; word-wrap: break-word !important;"><span style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;">自主博客</span></p></td><td valign="top" width="111" height="13" style="border-style: solid; border-color: rgb(0, 0, 0); margin: 0px; padding: 4px; word-break: break-all; max-width: 100%; word-wrap: break-word !important; box-sizing: border-box !important; background-color: rgb(190, 192, 191);"><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; max-width: 100%; clear: both; min-height: 1em; white-space: pre-wrap; box-sizing: border-box !important; word-wrap: break-word !important;"><span style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;">微信</span></p></td><td valign="top" width="112" height="13" style="border-style: solid; border-color: rgb(0, 0, 0); margin: 0px; padding: 4px; word-break: break-all; max-width: 100%; word-wrap: break-word !important; box-sizing: border-box !important; background-color: rgb(190, 192, 191);"><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; max-width: 100%; clear: both; min-height: 1em; white-space: pre-wrap; box-sizing: border-box !important; word-wrap: break-word !important;"><span style="margin: 0px; padding: 0px; max-width: 100%; font-stretch: normal; font-family: Helvetica; letter-spacing: 0px; box-sizing: border-box !important; word-wrap: break-word !important;">CSDN</span></p></td></tr><tr style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;"><td valign="top" width="111" height="39" style="border-style: solid; border-color: rgb(0, 0, 0); margin: 0px; padding: 4px; word-break: break-all; max-width: 100%; word-wrap: break-word !important; box-sizing: border-box !important; background-color: rgb(227, 228, 228);"><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; max-width: 100%; clear: both; min-height: 1em; white-space: pre-wrap; box-sizing: border-box !important; word-wrap: break-word !important;"><span style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;">天地會珠海分舵</span></p></td><td valign="top" width="112" height="39" style="border-style: solid; border-color: rgb(0, 0, 0); margin: 0px; padding: 4px; word-break: break-all; max-width: 100%; word-wrap: break-word !important; box-sizing: border-box !important;"><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; max-width: 100%; clear: both; min-height: 1em; white-space: pre-wrap; box-sizing: border-box !important; word-wrap: break-word !important;"><span style="margin: 0px; padding: 0px; max-width: 100%; font-stretch: normal; font-size: 11px; font-family: Helvetica; letter-spacing: 0px; box-sizing: border-box !important; word-wrap: break-word !important;"><a target=_blank href="http://techgogogo.com/">http://techgogogo.com</a></span><span style="margin: 0px; padding: 0px; max-width: 100%; font-family: Helvetica; font-size: 11px; letter-spacing: 0px; box-sizing: border-box !important; word-wrap: break-word !important;"> </span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; max-width: 100%; clear: both; min-height: 14px; white-space: pre-wrap; font-stretch: normal; font-family: Helvetica; box-sizing: border-box !important; word-wrap: break-word !important;"><br style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;" /></p></td><td valign="top" width="111" height="39" style="border-style: solid; border-color: rgb(0, 0, 0); margin: 0px; padding: 4px; word-break: break-all; max-width: 100%; word-wrap: break-word !important; box-sizing: border-box !important;"><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; max-width: 100%; clear: both; min-height: 1em; white-space: pre-wrap; box-sizing: border-box !important; word-wrap: break-word !important;"><span style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;">服務號</span><span style="margin: 0px; padding: 0px; max-width: 100%; font-stretch: normal; font-size: 10px; font-family: Helvetica; letter-spacing: 0px; box-sizing: border-box !important; word-wrap: break-word !important;">:TechGoGoGo</span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; max-width: 100%; clear: both; min-height: 1em; white-space: pre-wrap; box-sizing: border-box !important; word-wrap: break-word !important;"><span style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; word-wrap: break-word !important;">掃描碼</span><span style="margin: 0px; padding: 0px; max-width: 100%; font-stretch: normal; font-size: 10px; font-family: Helvetica; letter-spacing: 0px; box-sizing: border-box !important; word-wrap: break-word !important;">:</span></p><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; max-width: 100%; clear: both; min-height: 14px; white-space: pre-wrap; font-stretch: normal; font-family: Helvetica; box-sizing: border-box !important; word-wrap: break-word !important;"><img data-s="300,640" data-type="jpeg" data-src="http://mmbiz.qpic.cn/mmbiz/KYJTqcL56vuJuQArNAk7nsLW8hpxia6kjor2IEvib9RAQTEzzEPa4UngfjpT1GKIIKCnb7ib0IViaWEV7VFFiaAkkjg/0" data-ratio="1" data-w="125" _width="auto" src="http://mmbiz.qpic.cn/mmbiz/KYJTqcL56vuJuQArNAk7nsLW8hpxia6kjor2IEvib9RAQTEzzEPa4UngfjpT1GKIIKCnb7ib0IViaWEV7VFFiaAkkjg/640?tp=webp" style="max-width: 100%; margin: 0px; padding: 0px; height: auto !important; box-sizing: border-box !important; word-wrap: break-word !important; width: auto !important; visibility: visible !important;" alt="" /></p></td><td valign="top" width="112" height="39" style="border-style: solid; border-color: rgb(0, 0, 0); margin: 0px; padding: 4px; word-break: break-all; max-width: 100%; word-wrap: break-word !important; box-sizing: border-box !important;"><p style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px; max-width: 100%; clear: both; min-height: 1em; white-space: pre-wrap; color: rgb(62, 62, 62); font-family: 'Helvetica Neue', Helvetica, 'Hiragino Sans GB', 'Microsoft YaHei', ΢ÈíÑźÚ, Arial, sans-serif; font-size: 18px; line-height: 28.799×××370605px; box-sizing: border-box !important; word-wrap: break-word !important;"><span style="margin: 0px; padding: 0px; max-width: 100%; color: rgb(0, 0, 0); font-stretch: normal; font-size: 11px; font-family: Helvetica; letter-spacing: 0px; box-sizing: border-box !important; word-wrap: break-word !important;"><a target=_blank href="http://blog.csdn.net/zhubaitian">http://blog.csdn.net/zhubaitian</a></span><span style="margin: 0px; padding: 0px; max-width: 100%; color: rgb(0, 0, 0); font-family: Helvetica; font-size: 11px; letter-spacing: 0px; line-height: 28.799×××370605px; box-sizing: border-box !important; word-wrap: break-word !important;"> </span></p><div><span style="margin: 0px; padding: 0px; max-width: 100%; color: rgb(0, 0, 0); font-family: Helvetica; font-size: 11px; letter-spacing: 0px; line-height: 28.799×××370605px; box-sizing: border-box !important; word-wrap: break-word !important;"> </span></div></td></tr></tbody></table>
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。