亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android教你如何發(fā)現(xiàn)APP卡頓的實(shí)現(xiàn)

 更新時(shí)間:2020年11月01日 16:58:09   作者:huansky  
這篇文章主要介紹了Android教你如何發(fā)現(xiàn)APP卡頓的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧

最近部門(mén)打算優(yōu)化下 APP 在低端機(jī)上的卡頓情況,既然想優(yōu)化,就必須獲取卡頓情況,那么如何獲取卡頓情況就是本文目的。

一般主線(xiàn)程過(guò)多的 UI 繪制、大量的 IO 操作或是大量的計(jì)算操作占用 CPU,導(dǎo)致 App 界面卡頓。只要我們能在發(fā)生卡頓的時(shí)候,捕捉到主線(xiàn)程的堆棧信息和系統(tǒng)的資源使用信息,即可準(zhǔn)確分析卡頓發(fā)生在什么函數(shù),資源占用情況如何。那么問(wèn)題就是如何有效檢測(cè) Android 主線(xiàn)程的卡頓發(fā)生?

用 adb 系統(tǒng)工具觀察 App 的卡頓數(shù)據(jù)情況,試圖重現(xiàn)場(chǎng)景來(lái)定位問(wèn)題。

常用的方式是使用 adb SurfaceFlinger 服務(wù)和 adb gfxinfo 功能,在自動(dòng)化操作 app 的過(guò)程中,使用 adb 獲取數(shù)據(jù)來(lái)監(jiān)控 app 的流暢情況,發(fā)現(xiàn)出現(xiàn)出現(xiàn)卡頓的時(shí)間段,尋找出現(xiàn)卡頓的場(chǎng)景和操作。

方式1:adb shell dumpsysSurfaceFlinger

使用 ‘a(chǎn)db shell dumpsysSurfaceFlinger' 命令即可獲取最近 127 幀的數(shù)據(jù),通過(guò)定期執(zhí)行 adb 命令,獲取幀數(shù)來(lái)計(jì)算出幀率 FPS。

方式2:adb shell dumpsys gfxinfo

使用 ‘a(chǎn)db shell dumpsys gfxinfo' 命令即可獲取最新 128 幀的繪制信息,詳細(xì)包括每一幀繪制的 Draw,Process,Execute 三個(gè)過(guò)程的耗時(shí),如果這三個(gè)時(shí)間總和超過(guò) 16.6ms 即認(rèn)為是發(fā)生了卡頓。

已有的兩種方案比較適合衡量回歸卡頓問(wèn)題的修復(fù)效果和判斷某些特定場(chǎng)景下是否有卡頓情況,然而,這樣的方式有幾個(gè)明顯的不足:

  • 一般很難構(gòu)造實(shí)際用戶(hù)卡頓的環(huán)境來(lái)重現(xiàn);
  • 這種方式操作起來(lái)比較麻煩,需編寫(xiě)自動(dòng)化用例,無(wú)法覆蓋大量的可疑場(chǎng)景,測(cè)試重現(xiàn)耗時(shí)耗人力;
  • 無(wú)法衡量靜態(tài)頁(yè)面的卡頓情況;
  • 出現(xiàn)卡頓的時(shí)候app無(wú)法及時(shí)獲取運(yùn)行狀態(tài)和信息,開(kāi)發(fā)定位困難。

隨著對(duì)Android 源碼的深入研究,也有了其他兩種比較方便的方式,并且這兩種方式侵入性小,占用內(nèi)存低,能夠更好的用在實(shí)際場(chǎng)景中:

  • 利用UI線(xiàn)程的Looper打印的日志匹配;
  • 使用Choreographer.FrameCallback

利用 UI 線(xiàn)程的 Looper 打印的日志匹配

Android 主線(xiàn)程更新 UI。如果界面1秒鐘刷新少于 60 次,即 FPS 小于 60,用戶(hù)就會(huì)產(chǎn)生卡頓感覺(jué)。簡(jiǎn)單來(lái)說(shuō),Android 使用消息機(jī)制進(jìn)行 UI 更新,UI 線(xiàn)程有個(gè) Looper,在其 loop方法中會(huì)不斷取出 message,調(diào)用其綁定的 Handler 在 UI 線(xiàn)程執(zhí)行。如果在 handler 的 dispatchMesaage 方法里有耗時(shí)操作,就會(huì)發(fā)生卡頓。

下面來(lái)看下 Looper.loop( ) 的源碼

public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
      throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    // Allow overriding a threshold with a system prop. e.g.
    // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
    final int thresholdOverride =
        SystemProperties.getInt("log.looper."
            + Process.myUid() + "."
            + Thread.currentThread().getName()
            + ".slow", 0);

    boolean slowDeliveryDetected = false;

    for (;;) {
      Message msg = queue.next(); // might block
      if (msg == null) {
        // No message indicates that the message queue is quitting.
        return;
      }

      // This must be in a local variable, in case a UI event sets the logger
      final Printer logging = me.mLogging;
      if (logging != null) {
        logging.println(">>>>> Dispatching to " + msg.target + " " +
            msg.callback + ": " + msg.what);
      }
      // Make sure the observer won't change while processing a transaction.
      final Observer observer = sObserver;

      final long traceTag = me.mTraceTag;
      long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
      long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
      if (thresholdOverride > 0) {
        slowDispatchThresholdMs = thresholdOverride;
        slowDeliveryThresholdMs = thresholdOverride;
      }
      final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
      final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

      final boolean needStartTime = logSlowDelivery || logSlowDispatch;
      final boolean needEndTime = logSlowDispatch;

      if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
        Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
      }

      final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
      final long dispatchEnd;
      Object token = null;
      if (observer != null) {
        token = observer.messageDispatchStarting();
      }
      long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
      try {
        msg.target.dispatchMessage(msg);
        if (observer != null) {
          observer.messageDispatched(token, msg);
        }
        dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
      } catch (Exception exception) {
        if (observer != null) {
          observer.dispatchingThrewException(token, msg, exception);
        }
        throw exception;
      } finally {
        ThreadLocalWorkSource.restore(origWorkSource);
        if (traceTag != 0) {
          Trace.traceEnd(traceTag);
        }
      }
      if (logSlowDelivery) {
        if (slowDeliveryDetected) {
          if ((dispatchStart - msg.when) <= 10) {
            Slog.w(TAG, "Drained");
            slowDeliveryDetected = false;
          }
        } else {
          if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
              msg)) {
            // Once we write a slow delivery log, suppress until the queue drains.
            slowDeliveryDetected = true;
          }
        }
      }
      if (logSlowDispatch) {
        showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
      }

      if (logging != null) {
        logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
      }

      // Make sure that during the course of dispatching the
      // identity of the thread wasn't corrupted.
      final long newIdent = Binder.clearCallingIdentity();
      if (ident != newIdent) {
        Log.wtf(TAG, "Thread identity changed from 0x"
            + Long.toHexString(ident) + " to 0x"
            + Long.toHexString(newIdent) + " while dispatching to "
            + msg.target.getClass().getName() + " "
            + msg.callback + " what=" + msg.what);
      }

      msg.recycleUnchecked();
    }
  }

代碼中兩處標(biāo)紅的地方,就是 msg.target.dispatchMessage(msg) 的執(zhí)行前后索打印的 log。通過(guò)測(cè)量處理時(shí)間就能檢測(cè)到部分UI線(xiàn)程是否有耗時(shí)的操作。注意到這行執(zhí)行代碼的前后,有兩個(gè) logging.println 函數(shù),如果設(shè)置了logging,會(huì)分別打印出 ”>>>>> Dispatching to “ 和 ”<<<<< Finished to “ 這樣的日志,這樣我們就可以通過(guò)兩次log的時(shí)間差值,來(lái)計(jì)算 dispatchMessage 的執(zhí)行時(shí)間,從而設(shè)置閾值判斷是否發(fā)生了卡頓。

那么如何設(shè)置 logging 呢?

我們看下面的代碼:

/**
   * Control logging of messages as they are processed by this Looper. If
   * enabled, a log message will be written to <var>printer</var>
   * at the beginning and ending of each message dispatch, identifying the
   * target Handler and message contents.
   *
   * @param printer A Printer object that will receive log messages, or
   * null to disable message logging.
   */
public final class Looper { 
  private Printer mLogging; 
  public void setMessageLogging(@Nullable Printer printer) { 
    mLogging = printer; 
  } 
} 

public interface Printer { 
  void println(String x); 
}

Looper 的 mLogging 是私有的,并且提供了 setMessageLogging(@Nullable Printer printer) 方法,所以我們可以自己實(shí)現(xiàn)一個(gè) Printer,在通過(guò) setMessageLogging() 方法傳入即可,代碼如下:

public class BlockDetectByPrinter {
  
  public static void start() {
    Looper.getMainLooper().setMessageLogging(new Printer() {
      private static final String START = ">>>>> Dispatching";
      private static final String END = "<<<<< Finished";

      @Override
      public void println(String x) {
        if (x.startsWith(START)) {
          LogMonitor.getInstance().startMonitor();
        }
        if (x.startsWith(END)) {
          LogMonitor.getInstance().removeMonitor();
        }
      }
    });
  }
}

設(shè)置了logging后,loop方法會(huì)回調(diào) logging.println 打印出每次消息執(zhí)行的時(shí)間日志:”>>>>> Dispatching to “和”<<<<< Finished to “。BlockDetectByPrinter 的使用則在Application 的 onCreate 方法中調(diào)用 BlockDetectByPrinter.start() 即可。

我們可以簡(jiǎn)單實(shí)現(xiàn)一個(gè) LogMonitor 來(lái)記錄卡頓時(shí)候主線(xiàn)程的堆棧信息。當(dāng)匹配到 >>>>> Dispatching 時(shí),執(zhí)行 startMonitor,會(huì)在 200ms(設(shè)定的卡頓閾值)后執(zhí)行任務(wù),這個(gè)任務(wù)負(fù)責(zé)在子線(xiàn)程(非UI線(xiàn)程)打印UI線(xiàn)程的堆棧信息。如果消息低于 200ms 內(nèi)執(zhí)行完成,就可以匹配到 <<<<< Finished 日志,那么在打印堆棧任務(wù)啟動(dòng)前執(zhí)行 removeMonitor 取消了這個(gè)任務(wù),則認(rèn)為沒(méi)有卡頓的發(fā)生;如果消息超過(guò) 200ms 才執(zhí)行完畢,此時(shí)認(rèn)為發(fā)生了卡頓,并打印 UI 線(xiàn)程的堆棧信息。

LogMonitor如何實(shí)現(xiàn)?

public class LogMonitor {
  private static final String TAG = "LogMonitor";
  private static LogMonitor sInstance = new LogMonitor();
  private HandlerThread mLogThread = new HandlerThread("log");
  private Handler mIoHandler;
  private static final long TIME_BLOCK = 200L;

  private LogMonitor() {
    mLogThread.start();
    mIoHandler = new Handler(mLogThread.getLooper());
  }

  private static Runnable mLogRunnable = new Runnable() {
    @Override
    public void run() {
      StringBuilder sb = new StringBuilder();
      StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
      for (StackTraceElement s : stackTrace) {
        sb.append(s.toString() + "\n");
      }
      Log.e(TAG, sb.toString());
    }
  };

  public static LogMonitor getInstance() {
    return sInstance;
  }

  public boolean isMonitor() {
    return mIoHandler.hasCallbacks(mLogRunnable);
  }

  public void startMonitor() {
    mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK);
  }

  public void removeMonitor() {
    mIoHandler.removeCallbacks(mLogRunnable);
  }
}

這里我們使用 HandlerThread 來(lái)構(gòu)造一個(gè) Handler,HandlerThread 繼承自 Thread,實(shí)際上就一個(gè) Thread,只不過(guò)比普通的 Thread 多了一個(gè) Looper,對(duì)外提供自己這個(gè) Looper 對(duì)象的 getLooper 方法,然后創(chuàng)建 Handler 時(shí)將 HandlerThread 中的 looper 對(duì)象傳入。這樣我們的 mIoHandler 對(duì)象就是與 HandlerThread 這個(gè)非 UI 線(xiàn)程綁定的了,它處理耗時(shí)操作將不會(huì)阻塞UI。如果UI線(xiàn)程阻塞超過(guò) 200ms,就會(huì)在子線(xiàn)程中執(zhí)行 mLogRunnable,打印出 UI 線(xiàn)程當(dāng)前的堆棧信息,如果處理消息沒(méi)有超過(guò) 1000ms,則會(huì)實(shí)時(shí)的 remove 掉這個(gè)mLogRunnable 任務(wù)。

發(fā)生卡頓時(shí)打印出堆棧信息的大致內(nèi)容如下,開(kāi)發(fā)可以通過(guò) log 定位耗時(shí)的地方。

2020-10-30 14:26:13.823 30359-30415/com.example.myproxyplugin E/LogMonitor: java.lang.Thread.sleep(Native Method)
    java.lang.Thread.sleep(Thread.java:443)
    java.lang.Thread.sleep(Thread.java:359)
    com.example.myproxyplugin.MainActivity$1.run(MainActivity.java:22)
    android.os.Handler.handleCallback(Handler.java:900)
    android.os.Handler.dispatchMessage(Handler.java:103)
    android.os.Looper.loop(Looper.java:219)
    android.app.ActivityThread.main(ActivityThread.java:8347)
    java.lang.reflect.Method.invoke(Native Method)
    com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
    com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)

優(yōu)點(diǎn):用戶(hù)使用 app 或者測(cè)試過(guò)程中都能從app層面來(lái)監(jiān)控卡頓情況,一旦出現(xiàn)卡頓能記錄 app 狀態(tài)和信息, 只要dispatchMesaage執(zhí)行耗時(shí)過(guò)大都會(huì)記錄下來(lái),不再有前面兩種adb方式面臨的問(wèn)題與不足。

缺點(diǎn):需另開(kāi)子線(xiàn)程獲取堆棧信息,會(huì)消耗少量系統(tǒng)資源。

在實(shí)際實(shí)現(xiàn)中,不同手機(jī)不同 Android  系統(tǒng)甚至是不同的 ROM 版本,Loop 函數(shù)不一定都能打印出 ”>>>>> Dispatching to “ 和 ”<<<<< Finished to “ 這樣的日志,導(dǎo)致該方式無(wú)法進(jìn)行。

優(yōu)化的策略:我們知道 Loop 函數(shù)開(kāi)始和結(jié)束必會(huì)執(zhí)行 println 打印日志,所以?xún)?yōu)化版本將卡頓的判斷改為,Loop輸出第一句 log 時(shí)當(dāng)作 startMonitor,輸出下一句log時(shí)當(dāng)作end時(shí)刻來(lái)解決這個(gè)問(wèn)題。

其實(shí) Looper 中有個(gè) Observer 接口可以很好的完成這個(gè)任務(wù),只是因?yàn)楸粯?biāo)記為 hide 了,所以我們不能使用,不過(guò)可以知道下。

Observer 接口提供了三個(gè)方法,分別是監(jiān)聽(tīng)任務(wù)開(kāi)始,結(jié)束,發(fā)生錯(cuò)誤的回調(diào)。

  /** {@hide} */
  public interface Observer {
    /**
     * Called right before a message is dispatched.
     *
     * <p> The token type is not specified to allow the implementation to specify its own type.
     *
     * @return a token used for collecting telemetry when dispatching a single message.
     *     The token token must be passed back exactly once to either
     *     {@link Observer#messageDispatched} or {@link Observer#dispatchingThrewException}
     *     and must not be reused again.
     *
     */
    Object messageDispatchStarting();

    /**
     * Called when a message was processed by a Handler.
     *
     * @param token Token obtained by previously calling
     *       {@link Observer#messageDispatchStarting} on the same Observer instance.
     * @param msg The message that was dispatched.
     */
    void messageDispatched(Object token, Message msg);

    /**
     * Called when an exception was thrown while processing a message.
     *
     * @param token Token obtained by previously calling
     *       {@link Observer#messageDispatchStarting} on the same Observer instance.
     * @param msg The message that was dispatched and caused an exception.
     * @param exception The exception that was thrown.
     */
    void dispatchingThrewException(Object token, Message msg, Exception exception);
  }

利用Choreographer.FrameCallback監(jiān)控卡頓

Choreographer.FrameCallback 官方文檔鏈接(https://developer.android.com/reference/android/view/Choreographer.FrameCallback.html

我們知道, Android 系統(tǒng)每隔 16ms 發(fā)出 VSYNC 信號(hào),來(lái)通知界面進(jìn)行重繪、渲染,每一次同步的周期為16.6ms,代表一幀的刷新頻率。SDK 中包含了一個(gè)相關(guān)類(lèi),以及相關(guān)回調(diào)。理論上來(lái)說(shuō)兩次回調(diào)的時(shí)間周期應(yīng)該在 16ms,如果超過(guò)了 16ms 我們則認(rèn)為發(fā)生了卡頓,利用兩次回調(diào)間的時(shí)間周期來(lái)判斷是否發(fā)生卡頓(這個(gè)方案是 Android 4.1 API 16 以上才支持)。

這個(gè)方案的原理主要是通過(guò) Choreographer 類(lèi)設(shè)置它的 FrameCallback 函數(shù),當(dāng)每一幀被渲染時(shí)會(huì)觸發(fā)回調(diào) FrameCallback, FrameCallback 回調(diào) void doFrame (long frameTimeNanos) 函數(shù)。一次界面渲染會(huì)回調(diào) doFrame 方法,如果兩次 doFrame 之間的間隔大于 16.6ms 說(shuō)明發(fā)生了卡頓。

public class FPSFrameCallback implements Choreographer.FrameCallback {

  private static final String TAG = "FPS_TEST";
  private long mLastFrameTimeNanos = 0;
  private long mFrameIntervalNanos;

  public FPSFrameCallback(long lastFrameTimeNanos) {
    mLastFrameTimeNanos = lastFrameTimeNanos;
    // 1s 60 幀
    mFrameIntervalNanos = (long) (1000000000 / 60.0);
  }

  @Override
  public void doFrame(long frameTimeNanos) {

    //初始化時(shí)間
    if (mLastFrameTimeNanos == 0) {
      mLastFrameTimeNanos = frameTimeNanos;
    }
    final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
    if (jitterNanos >= mFrameIntervalNanos) {
      final long skippedFrames = jitterNanos / mFrameIntervalNanos;
      if (skippedFrames > 30) {
        Log.i(TAG, "Skipped " + skippedFrames + " frames! "
            + "The application may be doing too much work on its main thread.");
      }
    }
    mLastFrameTimeNanos = frameTimeNanos;
    //注冊(cè)下一幀回調(diào)
    Choreographer.getInstance().postFrameCallback(this);
  }
}

本質(zhì)和 log 沒(méi)太多區(qū)別,但是這個(gè)更加通用些,不會(huì)因?yàn)闄C(jī)型系統(tǒng)原因出現(xiàn)不可用的問(wèn)題。

示例

下面進(jìn)入實(shí)戰(zhàn),看看代碼層面是如何實(shí)現(xiàn)的。

MainActivity 代碼如下:

public class MainActivity extends AppCompatActivity {
  Handler handler = new Handler(Looper.getMainLooper());

  private final Runnable runnable = new Runnable() {
    @Override
    public void run() {
      try {
        Thread.sleep(600);
        handler.postDelayed(runnable, 500);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Choreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));
    BlockDetectByPrinter.start();
  }

  @Override
  protected void onResume() {
    super.onResume();
    handler.postDelayed(runnable, 500);
  }

}

收集到的堆棧信息如下:

2020-10-30 14:26:13.823 30359-30415/com.example.myproxyplugin E/LogMonitor: java.lang.Thread.sleep(Native Method)
    java.lang.Thread.sleep(Thread.java:443)
    java.lang.Thread.sleep(Thread.java:359)
    com.example.myproxyplugin.MainActivity$1.run(MainActivity.java:22)
    android.os.Handler.handleCallback(Handler.java:900)
    android.os.Handler.dispatchMessage(Handler.java:103)
    android.os.Looper.loop(Looper.java:219)
    android.app.ActivityThread.main(ActivityThread.java:8347)
    java.lang.reflect.Method.invoke(Native Method)
    com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
    com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)

對(duì)于 FPS log 可以看到如下信息:

     I/Choreographer: Skipped 64 frames!  The application may be doing too much work on its main thread.
     I/FPS_TEST: Skipped 65 frames!  The application may be doing too much work on its main thread.

如果你要把上面的方法用到自己的APP 中,那么還需要很多操作,具體可以閱讀參考文獻(xiàn)的內(nèi)容。

參考文章

廣研Android卡頓監(jiān)控系統(tǒng)

到此這篇關(guān)于Android教你如何發(fā)現(xiàn)APP卡頓的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Android APP卡頓內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Android編程實(shí)現(xiàn)通話(huà)錄音功能的方法

    Android編程實(shí)現(xiàn)通話(huà)錄音功能的方法

    這篇文章主要介紹了Android編程實(shí)現(xiàn)通話(huà)錄音功能的方法,結(jié)合實(shí)例形式較為詳細(xì)的分析了Android廣播接收機(jī)制實(shí)現(xiàn)錄音功能的操作技巧,需要的朋友可以參考下
    2017-06-06
  • Android實(shí)現(xiàn)檢測(cè)實(shí)體按鍵事件并屏蔽

    Android實(shí)現(xiàn)檢測(cè)實(shí)體按鍵事件并屏蔽

    這篇文章主要介紹了Android實(shí)現(xiàn)檢測(cè)實(shí)體按鍵事件并屏蔽 ,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-08-08
  • android中soap協(xié)議使用(ksoap調(diào)用webservice)

    android中soap協(xié)議使用(ksoap調(diào)用webservice)

    kSOAP是如何調(diào)用ebservice的呢,首先要使用SoapObject,這是一個(gè)高度抽象化的類(lèi),完成SOAP調(diào)用??梢哉{(diào)用它的addProperty方法填寫(xiě)要調(diào)用的webservice方法的參數(shù)
    2014-02-02
  • 解決android viewmodel 數(shù)據(jù)刷新異常的問(wèn)題

    解決android viewmodel 數(shù)據(jù)刷新異常的問(wèn)題

    這篇文章主要介紹了解決android viewmodel 數(shù)據(jù)刷新異常的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2020-03-03
  • Android Handler機(jī)制詳解原理

    Android Handler機(jī)制詳解原理

    Handler主要用于異步消息的處理:當(dāng)發(fā)出一個(gè)消息之后,首先進(jìn)入一個(gè)消息隊(duì)列,發(fā)送消息的函數(shù)即刻返回,而另外一個(gè)部分在消息隊(duì)列中逐一將消息取出,然后對(duì)消息進(jìn)行處理,也就是發(fā)送消息和接收消息不是同步的處理。 這種機(jī)制通常用來(lái)處理相對(duì)耗時(shí)比較長(zhǎng)的操作
    2021-11-11
  • Android啟動(dòng)頁(yè)面定時(shí)跳轉(zhuǎn)的三種方法

    Android啟動(dòng)頁(yè)面定時(shí)跳轉(zhuǎn)的三種方法

    這篇文章主要介紹了Android啟動(dòng)頁(yè)面定時(shí)跳轉(zhuǎn)的三種方法,實(shí)現(xiàn)打開(kāi)一個(gè)Android手機(jī)APP的歡迎界面后跳轉(zhuǎn)到指定界面的效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-11-11
  • Android RecyclerView區(qū)分視圖類(lèi)型的Divider的實(shí)現(xiàn)

    Android RecyclerView區(qū)分視圖類(lèi)型的Divider的實(shí)現(xiàn)

    本篇文章主要介紹了Android RecyclerView區(qū)分視圖類(lèi)型的Divider的實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-04-04
  • Android自定義實(shí)現(xiàn)一個(gè)車(chē)牌字母選擇鍵盤(pán)

    Android自定義實(shí)現(xiàn)一個(gè)車(chē)牌字母選擇鍵盤(pán)

    這篇文章主要為大家詳細(xì)介紹了Android如何自定義實(shí)現(xiàn)一個(gè)車(chē)牌字母選擇鍵盤(pán),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-06-06
  • Android:“萬(wàn)能”Activity重構(gòu)篇

    Android:“萬(wàn)能”Activity重構(gòu)篇

    本文主要介紹了mvp以及每一層,以及使用mvp來(lái)重構(gòu)“萬(wàn)能”Activity,其實(shí)每一層需要注意的東西還有很多,比如model層是最難寫(xiě)的一層。具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧
    2017-01-01
  • Android 模擬器(emulator-5554...)出現(xiàn)錯(cuò)誤解決辦法

    Android 模擬器(emulator-5554...)出現(xiàn)錯(cuò)誤解決辦法

    這篇文章主要介紹了Android 模擬器出現(xiàn)錯(cuò)誤解決辦法的相關(guān)資料,如:Unable to get view server version from device,F(xiàn)ailed to install helloworld.apk on device 'emulator-5554': timeout,這種常見(jiàn)錯(cuò)誤,解決辦法,需要的朋友可以參考下
    2016-11-11

最新評(píng)論