Backstage silent workers, explore service

As one of the four components of Android, service is an application component that can run for a long time in the background without providing an interface. The service can be started by other application components, and even if the user switches to other applications, the service will continue to run in the background. It should be noted that the service does not automatically start the thread, and all codes run in the main thread by default. Therefore, you need to manually create sub threads inside the service and perform specific tasks here, otherwise the main thread may be blocked.

Android multithreaded programming

Asynchronous message mechanism

Multithreading programming is actually consistent with Java. It can be implemented either by inheriting Thread or implementing Runnable interface. What you need to master in Android is to update the UI in the sub Thread. The UI is controlled by the main Thread, so the main Thread is also called UI Thread.

Only the original thread that created a view hierarchy can touch its views.

Although it is not allowed to update the UI in the sub thread, Android provides a set of asynchronous message processing mechanism, which perfectly solves the problem of operating the UI in the sub thread, that is, using the Handler. Let's review the usage of using Handler to update UI:

public class MainActivity extends AppCompatActivity {
    private static final int UPDATE_UI = 1001;
    private TextView textView;

    private Handler handler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(@NonNull Message msg) {
            if(msg.what == UPDATE_UI) textView.setText("Hello Thread!");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.tv_main);
    }

    public void updateUI(View view) {
        // new Thread(()-> textView.setText("Hello Thread!")).start(); Error!
        new Thread(()->{
            Message message = new Message();
            message.what = UPDATE_UI;
            handler.sendMessage(message);
        }).start();
    }
}

Using this mechanism can solve the problem of updating UI in sub threads. Let's analyze the working principle of Android asynchronous Message processing mechanism: asynchronous Message processing in Android is mainly composed of four parts: Message, Handler, MessageQueue and Looper.
1. Message: a message passed between threads. It can carry a small amount of information internally, which is used to exchange data between different threads.
2. Handler: handler, which is mainly used to send and process messages. Sending a message usually uses the handler's sendMessage() method, and after a series of tossing and turning, the sent message will eventually be delivered to the handler's handleMessage() method.
3. MessageQueue: message queue, which is mainly used to store all messages sent through the Handler. This part of the message will always exist in the message queue and wait to be processed. There will only be one MessageQueue object per thread.

4. Looper is the steward of MessageQueue in each thread. After calling Looper's loop() method, it will enter an infinite loop. Then whenever a message is found in MessageQueue, it will be taken out and passed to Handler's handleMessage() method. There will also be only one looper object per thread.

The whole process of asynchronous Message processing: first, you need to create a Handler object in the main thread and rewrite the handleMessage() method. Then, when UI operations are required in the child thread, a Message object is created and the Message is sent out through the Handler. After that, the Message will be added to the queue of MessageQueue for processing, while Looper will always try to get the Message to be processed from MessageQueue, and finally distribute it back to the Handler's handleMessage() method. Since the Handler is created in the main thread, the code in the handleMessage() method will also run in the main thread, so we can safely operate the UI here. The flow of the whole asynchronous Message processing mechanism is shown in the figure below:

AsyncTask

However, in order to make it easier for us to operate the UI in the sub thread, Android also provides some other useful tools, such as AsyncTask. The implementation principle behind AsyncTask is also based on asynchronous message processing mechanism, but Android has done a good encapsulation for us. First, let's take a look at the basic usage of AsyncTask. Because AsyncTask is an abstract class, if we want to use it, we must create a subclass to inherit it. During inheritance, we can specify three generic parameters for AsyncTask class. The purposes of these three parameters are as follows:

Params: parameters that need to be passed in when executing AsyncTask, which can be used in background tasks.
Progress: if the current progress needs to be displayed on the interface during background task execution, the generic specified here is used as the progress unit.
Result: if the result needs to be returned after the task is executed, the generic type specified here is used as the return value type.

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private final int REQUEST_EXTERNAL_STORAGE = 1;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void startDownload(View view) {
        verifyStoragePermissions(this);
        ProgressBar progressBar = findViewById(R.id.download_pb);
        TextView textView = findViewById(R.id.download_tv);
        new MyDownloadAsyncTask(progressBar, textView).execute("http://xxx.zip");
    }


    class MyDownloadAsyncTask extends AsyncTask<String, Integer, Boolean> {
        private ProgressBar progressBar;
        private TextView textView;

        public MyDownloadAsyncTask(ProgressBar progressBar, TextView textView) {
            this.progressBar = progressBar;
            this.textView = textView;
        }

        @Override
        protected Boolean doInBackground(String... strings) {
            String urlStr = strings[0];
            try {
                URL url = new URL(urlStr);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                InputStream inputStream = conn.getInputStream();
                // Get the total length of the file
                int length = conn.getContentLength();
                File downloadsDir = new File("...");
                File descFile = new File(downloadsDir, "xxx.zip");
                int downloadSize = 0;
                int offset;
                byte[] buffer = new byte[1024];
                FileOutputStream fileOutputStream = new FileOutputStream(descFile);
                while ((offset = inputStream.read(buffer)) != -1){
                    downloadSize += offset;
                    fileOutputStream.write(buffer, 0, offset);
                    
                    // Throw out the progress of task execution
                    publishProgress((downloadSize * 100 / length));
                }
                fileOutputStream.close();
                inputStream.close();
                Log.i(TAG, "download: descFile = " + descFile.getAbsolutePath());
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
            return true;
        }

        // Perform result processing in the main thread
        @Override
        protected void onPostExecute(Boolean aBoolean) {
            super.onPostExecute(aBoolean);
            if(aBoolean){
                textView.setText("Download complete, file at..xx.zip");
            }else{
                textView.setText("Download failed");
            }
        }

        // Task progress update
        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            // Receive the new progress and execute the processing
            textView.setText("Downloaded" + values[0] + "%");
            progressBar.setProgress(values[0]);
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            textView.setText("No Click to download");
        }
    }
}

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-hvjsjhsw-1607391406799)( https://img.zouchanglin.cn/20201207184837.gif-zouchanglin.cn )]

1. onPreExecute(): the method will be called before the background task starts to execute to perform some interface initialization operations, such as displaying a progress bar dialog box.

2. doInBackground(): all the code in the method will run in the sub thread. We should handle all the time-consuming tasks here. Once the task is completed, you can return the execution result of the task through the return statement. If the third generic parameter of AsyncTask specifies Void, you can not return the execution result of the task. Note that UI operations are not allowed in this method. If you need to update UI elements, such as feedback on the execution progress of the current task, you can call the publishProgress() method to complete it.

3. onProgressUpdate(): when the publishProgress() method is called in the background task, onProgressUpdate() method will be called soon. The parameters carried in this method are passed in the background task. In this method, the UI can be operated, and the interface elements can be updated accordingly by using the values in the parameters.

4. onPostExecute(): this method will be called soon after the background task is completed and returned through the return statement. The returned data will be passed to this method as parameters. You can use the returned data to carry out some UI operations, such as reminding the result of task execution and closing the progress bar dialog box.

Basic usage of services

First of all, as one of Android, services should also be declared in the Manifest file, which is a common feature of the four Android components. Create a new MyService class, inherit from Service, and then declare it in the Manifest file.

Service creation and startup

MyService.java:

public class MyService extends Service {
    private static final String TAG = "MyService";

    public MyService() {
        
    }
    
    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind: ");
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="cn.tim.basic_service">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        
        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="true" />

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

You can see that there are two attributes in the service tag of MyService. The exported attribute indicates whether to allow other programs except the current program to access the service, and the enabled attribute indicates whether to enable the service. Then in mainactivity Start this service in Java:

// Start service
startService(new Intent(this, MyService.class));

Stop (destroy) of service

How to stop the service? In mainactivity Stop this service in Java:

Intent intent = new Intent(this, MyService.class);
// Start service
startService(intent);
// Out of Service
stopService(intent);

In fact, Service can also override other methods:

public class MyService extends Service {
    private static final String TAG = "MyService";

    public MyService() {
    }

    // establish
    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate: ");
    }

    // start-up
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand: ");
        return super.onStartCommand(intent, flags, startId);
    }

    // binding
    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind: ");
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    // Unbound
    @Override
    public void unbindService(ServiceConnection conn) {
        super.unbindService(conn);
        Log.i(TAG, "unbindService: ");
    }

    // Destroy
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "onDestroy: ");
    }
}

[the external chain image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-7wlmojjs-1607391406802)( https://img.zouchanglin.cn/20201207222459.png-zouchanglin.cn )]

In fact, onCreate() method is called when the service is first created, while onStartCommand() method is called every time the service is started. Since we just clicked the Start Service button for the first time and the service has not been created at this time, both methods will be executed. If we click the Start Service button several times in succession, only onStartCommand() method can be executed:

[the external link image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-nikhfyhi-1607391406803)( https://img.zouchanglin.cn/20201207230432.png-zouchanglin.cn )]

Service binding and unbinding

In the above example, although the service is started in the activity, after the service is started, the activity has little relationship with the service. This is similar to the event notification service: you can start it! Then the service is busy with its own affairs, but the activity does not know what the service has done and how it has been completed. Therefore, it is necessary to use service binding.

For example, a download function is provided in MyService, and then you can decide when to start downloading and check the download progress at any time. The idea to realize this function is to create a special Binder object to manage the download function and modify MyService java:

public class MyService extends Service {
    private static final String TAG = "MyService";

    private DownloadBinder mBinder = new DownloadBinder();
    
    static class DownloadBinder extends Binder {
        public void startDownload() {
            // Simulation starts downloading
            Log.i(TAG, "startDownload executed");
        }

        public int getProgress() {
            // Simulation return download progress
            Log.i(TAG, "getProgress executed");
            return 0;
        }
    }

    public MyService() {}

    // establish
    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate: ");
    }

    // start-up
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand: ");
        return super.onStartCommand(intent, flags, startId);
    }

    // binding
    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind: ");
        return mBinder;
    }

    // Unbound
    @Override
    public void unbindService(ServiceConnection conn) {
        super.unbindService(conn);
        Log.i(TAG, "unbindService: ");
    }

    // Destroy
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "onDestroy: ");
    }
}

MainActivity.java is as follows:

public class MainActivity extends AppCompatActivity {

    private MyService.DownloadBinder downloadBinder;

    ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            downloadBinder = (MyService.DownloadBinder) service;
            downloadBinder.startDownload();
            downloadBinder.getProgress();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void aboutService(View view) {
        int id = view.getId();
        Intent intent = new Intent(this, MyService.class);
        switch (id){
            case R.id.start_btn:
                startService(intent);
                break;
            case R.id.stop_btn:
                stopService(intent);
                break;
            case R.id.bind_btn:
                // Pass in bind here_ AUTO_ Create means that the service is automatically created after the activity and service are bound
                bindService(intent, connection, BIND_AUTO_CREATE);
                break;
            case R.id.unbind_btn:
                unbindService(connection);
                break;
        }
    }
}

The anonymous class of ServiceConnection overrides the onServiceConnected() method and onServiceDisconnected() method, which will be called when the activity and service are successfully bound and unbound respectively. In the onServiceConnected() method, the DownloadBinder instance is obtained through downward transformation. With this instance, the relationship between activities and services becomes very close. Now we can call any public() method in the DownloadBinder according to the specific scenario in the activity, that is, the function of commanding the service to do what the service does (although the implementation of startDownload and getProgress is very simple).

It should be noted that any service is common in the whole application, that is, MyService can bind not only to MainActivity, but also to any other activity, and they can get the same DownloadBinder instance after binding.

Service life cycle

Once the startServices() method is called, the corresponding Service will be started and the callback onStartCommand() will be called. If the Service is not created, onCreate() will be called to create the Service object. After the Service is started, it will remain running until the stopService() or stopSelf() method is called. No matter how many times startService() is called, oncreate () method will not be executed as long as the Service object exists, so you only need to call stopService() or stopSelf() method once to stop the corresponding Service.

When obtaining the persistent connection of a service through bindService(), the onBind() method in the service will be called back. Similarly, if the service has not been created before, the oncreate() method will execute before the onBind() method. After that, the caller can get the instance of the IBinder object returned in the onBind() method, so that he can freely communicate with the service. As long as the connection between the caller and the service is not disconnected, the service will remain running.

In this case, how can the service be destroyed when the startService() method and the bindService() method are called? According to the mechanism of Android system, a service will always be running as long as it is started or bound. The service can only be destroyed if the above two conditions are not met at the same time. Therefore, in this case, the onDestroy() method will not execute until stopService() and unbindService() methods are called at the same time.

More tips for service

The above describes the most basic usage of services. Let's take a look at more advanced skills about services.

Use front desk service

Almost all services run in the background, and the system priority of services is still relatively low. When the system runs out of memory, it is possible to recycle the services running in the background. If you want to keep the service running all the time without being recycled due to insufficient system memory, you can use the foreground service. For example, the suspended window of QQ phone, or some weather applications need to display the weather in the status bar.

public class FrontService extends Service {
    String mChannelId = "1001";

    public FrontService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
        Notification notification = new NotificationCompat.Builder(this, mChannelId)
                .setContentTitle("This is content title.")
                .setContentText("This is content text.")
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.mipmap.ic_launcher)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(),
                        R.mipmap.ic_launcher))
                .setContentIntent(pi)
                .build();
        startForeground(1, notification);
    }
}

Using IntentService

The code in the service runs in the main thread by default. If you deal with some time-consuming logic directly in the service, ANR is easy to occur. Therefore, multi-threaded programming is required. In case of time-consuming operations, you can start a sub thread in each specific method of the service, and then deal with those time-consuming logic here. It can be written as follows:

public class OtherService extends Service {
    public OtherService() {}

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(()->{
            // TODO performs time-consuming operations
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
	...
}

However, once the service is started, it will always be running. You must call stopService() or stopSelf() to stop the service. Therefore, if you want to realize the function of automatically stopping a service after execution, you can write as follows:

public class OtherService extends Service {
    public OtherService() {}

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(()->{
            // TODO performs time-consuming operations
            stopSelf();
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }
	...
}

Although this writing method is not complicated, there will always be some programmers who forget to start the thread or call the stopSelf() method. In order to simply create an asynchronous service that will stop automatically, Android specifically provides an IntentService class, which solves the two embarrassments mentioned above. Let's take a look at its usage:

MyIntentService.java

public class MyIntentService extends IntentService {
    private static final String TAG = "MyIntentService";
    private int count = 0;
    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        count++;
        Log.i(TAG, "onHandleIntent: count = " + count);
    }
}

MainActivity.java:

for (int i = 0; i < 10; i++) {
    Intent intent = new Intent(MainActivity.this, MyIntentService.class);
    startService(intent);
}

reference material: First line code

Original address: "Silent workers in the background, exploring service"

Tags: Android

Posted by ZachEdwards on Tue, 03 May 2022 09:29:40 +0300