-1

How can i optimize my stopwatch in my app to use less cpu?

I am running a stopwatch using an asynctask class in the mainActivity. The doInBackground() method increments the values for the hours, minutes, seconds and centiseconds(10th of a second). The onProgressUpdate() method is responsible for updating 4 imageViews that display the hrs, mins, sec, centisec.

The problem i have is that the stopwatch uses about on average 50%+ cpu usage according to android studio(50% user and 30% kernel usage) and a cpu monitoring app that i installed on the device (2013 HTC one m7). The default android operating system stopwatch uses only about 10% cpu usage. If i use textViews instead of image views the cpu usage drop to half (less than 25%). But it is still more than 10% and i also i want to keep the style of digits im using. image Would caching the images help in anyway? source

I have also considered using XML drawables for the digits instead of bitmaps, but i don't know how effective this will be or if its even possible to create xml drawables of the digits

Lend me your knowledge stackoverflow

main XML

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#763768"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.example.aroboius.stopwatch.MainActivity"
    tools:showIn="@layout/activity_main">

    <ImageView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:id="@+id/hoursImage"
        android:layout_marginRight="20dp"
        android:src="@drawable/digit00" />


    <ImageView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:id="@+id/minutesImage"
        android:layout_marginRight="20dp"
        android:src="@drawable/digit00" />


    <ImageView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:id="@+id/secondsImage"
        android:layout_marginRight="20dp"
        android:src="@drawable/digit00" />

    <ImageView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:id="@+id/centiSecondsImage"
        android:src="@drawable/digit00" />

</LinearLayout>

MainActivity

public class MainActivity extends AppCompatActivity {

    ImageView hoursIMG, minutesIMG, secondsIMG, centiSecondsIMG;
    TextView hoursText, minutesText, secondsText, centicsecondsText;
    int centiseconds, seconds, minutes, hours ;
    long startMS , endMS , elapsed ;
    boolean timerRunning;

    String [] digit = {"digit00","digit01","digit02","digit03","digit04","digit05","digit06","digit07","digit08","digit09", "digit10", "digit11","digit12","digit13","digit14","digit15","digit16","digit17","digit18","digit19","digit20",   "digit21","digit22","digit23","digit24","digit25","digit26","digit27","digit28","digit29","digit30","digit31",
            "digit32","digit33","digit34","digit35","digit36","digit37","digit38","digit39","digit40","digit41","digit42","digit43","digit44","digit45","digit46","digit47","digit48","digit49","digit50","digit51","digit52","digit53",
            "digit54","digit55","digit56","digit57","digit58","digit59","digit60","digit61","digit62","digit63","digit64","digit65","digit66","digit67","digit68","digit69","digit70","digit71","digit72","digit73","digit74","digit75",
            "digit76","digit77","digit78","digit79","digit80","digit81","digit82","digit83","digit84","digit85","digit86","digit87","digit88","digit89", "digit90","digit91","digit92","digit93","digit94","digit95","digit96","digit97","digit98","digit99"} ;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        //initializing values
        centiseconds = 0; seconds = 0; minutes = 0; hours = 0;
        startMS = 0; endMS = 0; elapsed = 0;

        hoursIMG = (ImageView) findViewById(R.id.hoursImage);
        minutesIMG = (ImageView) findViewById(R.id.minutesImage);
        secondsIMG = (ImageView) findViewById(R.id.secondsImage);
        centiSecondsIMG = (ImageView) findViewById(R.id.centiSecondsImage);

        //start asynctask/stopwatch
        timerRunning = true; new asyncTask().execute();
    }

    class asyncTask extends AsyncTask<Void, Void, Void> {

        //initialize a variable to the current system time
        @Override
        protected void onPreExecute() {
            startMS = System.currentTimeMillis();
        }

        @Override
        protected Void doInBackground(Void... params) {

            //timerRunning a varible to stop/start the timer
            while (timerRunning) {

                //initialize a 2nd variable to the current system time
                endMS = System.currentTimeMillis();

                //get the difference between the 2 time variables
                elapsed = endMS - startMS;

                //once it is greater than or equal to 100ms increment the centis, mins, secs, hrs
                if (elapsed >= 100) {

                    //reset the starting variable to repeat the process. it also compensating if elapses is greater than 100ms
                    startMS = endMS - (elapsed - 100);

                    centiseconds++;

                    if (centiseconds > 9) {
                        centiseconds = 0;
                        seconds++;
                        if (seconds > 59) {
                            seconds = 0;
                            minutes++;
                            if (minutes > 59) {
                                minutes = 0;
                                hours++;
                            }
                        }
                    }

                    //call method to update the images
                    publishProgress();
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Void... values) {

 //get resource IDs for images that represent the values of hrs, mins, secs using the string array created earlier

                int hourResID = getResources().getIdentifier(digit[hours], "drawable", getPackageName());
                int minResID= getResources().getIdentifier(digit[minutes], "drawable", getPackageName());
                int secResID= getResources().getIdentifier(digit [seconds], "drawable", getPackageName());
                int csecResID= getResources().getIdentifier(digit[centiseconds], "drawable", getPackageName());

//set images of imageViews
                centiSecondsIMG.setImageResource(csecResID);
                secondsIMG.setImageResource(secResID);
                minutesIMG.setImageResource(minResID);
                hoursIMG.setImageResource(hourResID);
        }
    }
}

1:

Madhur
  • 3,303
  • 20
  • 29
nebuchadnezzar I
  • 125
  • 1
  • 12

2 Answers2

1

Instead of burning your CPU and battery with a loop, you should use a CountDownTimer

  new CountDownTimer(30000, 1000) { // 30sec, tick each second

     public void onTick(long millisUntilFinished) {
         publishProgress();
         // mTextField.setText("seconds remaining: " + millisUntilFinished / 1000);
     }

     public void onFinish() {
         mTextField.setText("done!");
     }
  }.start();

Or you can use a Runnable that you delay every time by the desired amount of time, let's say 200ms:

final static long REFRESH_RATE = 200L;
Handler mHandler = new Handler();

private final Runnable mRunnable = new Runnable() {
    @Override
    public void run() {
        if (mStarted) {
            long seconds = (System.currentTimeMillis() - t) / 1000;
            statusBar.setText(String.format("%02d:%02d", seconds / 60, seconds % 60));

            // cancel previous messages if they exist
            handler.removeCallbacks(mRunnable);

            handler.postDelayed(runnable, REFRESH_RATE);
        }
    }
};

start it:

mHandler.postDealyed(runnable, 0);

You can use a Timer with fixed rate:

new Timer().scheduleAtFixedRate(new TimerTask(){
    @Override
    public void run(){
       publishProgress();
    }
},0,1000);

Or you can use a ScheduledExecutorService, that will fix most of the problems you mentioned. See here and here.

ScheduledExecutorService scheduledExecutorService =
        Executors.newScheduledThreadPool(1);
long lastSecondDisplayed = 0;

ScheduledFuture scheduledFuture =
    scheduledExecutorService.schedule(new Callable() {
        public Object call() throws Exception {
            long now = System.currentTimeMillis() / 1000;

            // add this optimisation, so you don't calculate and
            // for sure don't refresh your UI (even slower)
            // if it's not needed:
            if (lastSecondDisplayed != now) {
                lastSecondDisplayed = now;
                // calculate whatever you want
                publishProgress();
            }
            return "Called!";
        }
    }, 1, TimeUnit.SECONDS);

Optimisations:

  • move the 4 getResources().getIdentifier(... lines out of onProgressUpdate and prepare the 10 digits only once in onCreate.

It is always good to reuse resources in java, because when you're creating and disposing them frequently, like here, you'll finish your memory quite fast and the GC will have to free some memory for you. Both creating the objects, and especially garbage-collecting them takes a fair amount of time. By creating them only once and reusing them you keep yourself far from all this trouble.

Community
  • 1
  • 1
Gavriel
  • 18,880
  • 12
  • 68
  • 105
  • it is giving me an error saying return type required. Also this is a timer not a stopwatch, am i able to manipulate this to count up? – nebuchadnezzar I Feb 02 '16 at 07:12
  • i have tried the countdown timer and cpu usage is still fairly high around 35%. Runnables and Threads i have already used in the past (their delays are inconsistent and time is lost in them) also when in the background the handlers messege queue fills up and when you return the stopwatch speeds up to try display all the messeges that were queued, they also show the same result in regards to cpu usage. Either way thank you for your input. – nebuchadnezzar I Feb 02 '16 at 07:37
  • See my update. You can try with text instead of images and 1000ms to see updates only once per second, to have a base to compare it to. – Gavriel Feb 02 '16 at 08:04
  • i think the scheduledexecutionor service was what i needed. i will need to do some extra tests and before i mark your answer as the correct one. ps. What is the difference between "User" cpu usage and "Kernel" cpu usage in android studio? – nebuchadnezzar I Feb 02 '16 at 11:43
  • the "new callable" line doesn't work because the parameter needs to be a runnable, any new Runnable i pass to the service doesnt run at all unless if i put "MainActivity.this.runOnUiThread(new Runnable()....run(){}" inside the runnable. what is the problem? i searched all the links you gave me and even more – nebuchadnezzar I Feb 03 '16 at 07:10
  • Correct, the 4 lines to setImageResource need to run on the UI thread, but only those. All the rest(including the one-time preparation of the resources, calculations) run in the background or beforehand. I would also create that Runnable once and keep the reference and reuse it by just passing it to runOnUiThread again and again. Create everything possible in advance and only once, so the garbage collector doesn't need to clean those things up all the time. – Gavriel Feb 03 '16 at 08:57
  • i created a scheduledexecutioner task that fires every 100ms, in its run method i do calculations for the timer (occurs in the background thread?), then in the same run method i callMainActivity.this.runOnUiThread( new Runnable) whose method updates the images. Everthing works perfectly for about 20s, then cpu usage jumps from around 8% to 30 and the digits update irregularly. does this have to do with the garbage collector that i know nothing about? – nebuchadnezzar I Feb 03 '16 at 10:04
  • It is very much possible. Run the app in AndroidStudio, open the AndroidMonitor tab at the bottom, inside it the Memory tab. You'll see how "high" the memory usage is. If at the same time it starts to freak out after 20 seconds you see that the used memory drops a bit, then the GC ran. It doesn't necessarily mean though that it is caused by this part of your code. However if you do not do the recommended optimizations to minimize object creation and throwing them away (which causes the GC to have more work to do) then it makes things worse for sure – Gavriel Feb 03 '16 at 16:29
  • I also added the optimisation with the lastSecondDisplayed to the last block of code, try that as well – Gavriel Feb 03 '16 at 16:37
  • Gavriel you are right about the memory, it spazzes out at the same time as cpu usage jump, memory graph has several peaks and looks like a sharp sin^2 wave. I dont know how to apply your optimization because instead of using schedule i use scheduleAtFixedRate and a runnable instead of callable. thank you for your help, this problem that seems so simple has stopped my momentum dead in its track :'D. how does the android OS do it in its stopwatch? – nebuchadnezzar I Feb 03 '16 at 19:10
  • So at least accept my answer:) BTW all the optimisations are general and you can use them with your Runnable as well. – Gavriel Feb 03 '16 at 20:54
-3

It seems it was a problem with continuously resetting the imageViews to different drawables. The getResources().getIdentifier() function calls also somewhat contributed to extra cpu usage and GarbageCleaner(GC) problems.

Instead of creating an image resource array I created a drawable array that I can continually reference. I created it in onCreate().

 final Drawable[] drawable = {ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit00),
                ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit01),
                ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit02),
                ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit03),
                ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit04),
                ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit05),
                ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit06),
                ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit07),
                ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit08),
                ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit09),
                ContextCompat.getDrawable(getApplicationContext(), R.drawable.digit10)}

Then I set the images on the imageViews using the Drawable

MainActivity.this.runOnUiThread(new Runnable() {
                                @Override
                                public void run() {

                                    centiSecondsIMG.setImageDrawable(drawable[centiseconds]);
                                    secondsIMG.setImageDrawable(drawable[seconds]);
                                    minutesIMG.setImageDrawable(drawable[minutes]);
                                    hoursIMG.setImageDrawable(drawable[hours]);


                                }
                            });

Memory and cpu are now all perfectly fine and working normally.

i sitll dont know why changing the imageViews images rapidly using setImageResource() caused problems with cpu,memory and GC.

nebuchadnezzar I
  • 125
  • 1
  • 12