📜 ⬆️ ⬇️

Android Process: I gave birth to you, I you and ...



Have you ever thought about what happens to your application after the system has killed its process as unnecessary? Sadly, many people don’t even worry about it, as if it would happen in a parallel universe, and it doesn’t concern them. Beginners are especially affected. Their blind faith in the steadfastness of link statics is simply amazing.

In this article I will talk about some of the mistakes that may arise as a result of violation of the sixth commandment (do not kill) in relation to the application process, and how to check how well it returns from the next world.

It's no secret that the process can be killed by the system. And you were interested in, is it possible to imitate it? You can try to “unleash” the system on your application, launching a bunch of other applications that eat up a notable piece of memory, and then hope that the system did come down to us by killing the desired application. Directly some kind of shamanism turns out, and this is the path of the admins, but not the programmers. I was interested in how you can quickly and easily kill the application, so much as if the system did it to release resources. After all, if we succeed in repeating such behavior in “laboratory conditions”, it will be possible to catch a lot of errors at the development stage, or easily reproduce them to identify the causes.
')
As it turned out, the necessary mechanism is already available in the SDK, and this is ... drumming ... The "Terminate Application" button.



Clicking on it is similar to executing the following code:
//       . // ,   "Terminate Application"   “1”. System.exit(1); 
This, in fact, kills the process. A similar action occurs when the system tries to get rid of an unnecessary process.

In order to reproduce the situation with the resumption of the application after it is completely stopped, the following sequence of actions should be done:
  1. Using Android Stidio run the app;
  2. Minimize the application by pressing the “Home” button;
  3. In Android Studio, go to the Android tab, select the application and click the "Terminate Application" button;
  4. Deploy a minimized application.

If the Activity does not quite correctly handle the restore after destruction, you will immediately notice it. At best, it will fall, at worst, it will hang.

So, we have learned to lure one of the most secretive types of errors. Let's learn to foresee these mistakes. Let's start with the most obvious cases, and then move on to less unambiguous cases.

Situation 1: Static is not reliable


Imagine this situation: In one Activity, we enter a certain set of data, for example, a first and last name. This Activity strictly monitors the correctness of data entry, so it is possible to guarantee their “correctness” with firm confidence, and in general that they have been entered.
First Activity
 public class StaticActivityFirst extends BaseActivity { private EditText mEditFirstName; private EditText mEditLastName; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.sit01_0_activity_first); mEditFirstName = (EditText) findViewById(R.id.editFirstName); mEditLastName = (EditText) findViewById(R.id.editLastName); } public void onNextClick(View view) { String firstName = mEditFirstName.getText().toString().trim(); //      if (TextUtils.isEmpty(firstName)) mEditFirstName.setError(getString(R.string.sit01_0_empty_text_error_message)); else { mEditFirstName.setError(null); String lastName = mEditLastName.getText().toString().trim(); //      if (TextUtils.isEmpty(lastName)) mEditLastName.setError(getString(R.string.sit01_0_empty_text_error_message)); else { mEditLastName.setError(null); StaticActivitySecond.sPerson = new Person(firstName, lastName); startActivity(new Intent(this, StaticActivitySecond.class)); } } } } 
After clicking the “Next” button, an object is created that stores the entered data and a link is recorded in the static, and then the second Activity is opened, which shows this data in the appropriate form. For example, writes: "Your name is Vladimir Putin."
Second Activity
 public class StaticActivitySecond extends BaseActivity { public static Person sPerson; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.sit01_0_activity_second); TextView txtSummary = (TextView) findViewById(R.id.txtSummary); txtSummary.setText(String.format(getString(R.string.sit01_0_summary_message), sPerson.firstName, sPerson.lastName)); } } 
As you can see from the code, in the first Activity the developer decided not to “bother” and put the data in static variables. This is very tempting when you need to transfer a rather extraordinary object that is not amenable to simple serialization. He also did not think long over the second Activity and did not check for zero. Why, when the first Activity has already done everything?

We perform the sequence described earlier for correctly destroying the process for the second Activity. Application falls.

What happened?
The problem here is not the absence of a check for null. The worst problem is the loss of user data. Static objects should not be a data warehouse, especially if this is their only place. This applies to both ordinary variables and singletons. So if something important is stored in statics, be prepared to lose this important one at any moment.

What to do?
The presence of such errors often indicates a low qualification of the programmer, or an excessively high sense of laziness. About how to do correctly, it is written a huge number of tutorials. In this example, the best way is to transfer data through a bundle. You can also write this data in SharedPreferences, or you should think about creating a database.
Important: Do not forget that singleton is also a static variable. If you use singleton, then it should act only as a tool to facilitate access to data, but not to be the only repository for them.

Magic Power Application
How often do I see tips to use the Application class as a singleton or to initialize a singleton in the onCreate () method of the Application class. Allegedly, after that he will become steeper than Lenin, that is, he will be more alive than all living under any circumstances. Perhaps this is a special case of error found only by me. Moreover, all the publications that I found, clearly do not declare such properties of Singleton. Some of them say that a singleton can be destroyed by the garbage collector if you initiate it in the Activity class (which sounds a little crazy to me). In others, they are afraid of unloading the class from the classifier (and this already seems to be true).

Now I am not going to find out what is true here and what is speculation. In any case, this only reduces the likelihood of losing static links, but neither does it help to stop the process. Stopping the process will lead to the complete destruction of the classifier, and with it the destruction of all classes, including the class Application.

Situation 2: setRetainInstance as a solution to all problems


Suppose a certain programmer decided to repeat the example with the name and surname, but using the dialogue of fragments.
So, there is a DialogFragment with a text box for entering the last name and first name. This fragment can be assigned a listener for data entry. In order not to lose the reference to the listener in onCreate () is called setRetainInstance (true).
Part of the dialogue code
 public class Situation2DialogFragment extends DialogFragment { private OnPersonChooseListener mListener; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } public void setListener(OnPersonChooseListener listener) { mListener = listener; } public void invokePersonChoose(Person person) { mListener.onPersonChoose(person); } public interface OnPersonChooseListener { public void onPersonChoose(Person person); } // ... } 

There is a button on the screen, when clicked, this dialog is displayed.
Calling a dialogue from the Activity
 public void onChoosePersonClick(View view) { Situation2DialogFragment dialog = new Situation2DialogFragment(); dialog.setListener(new Situation2DialogFragment.OnPersonChooseListener() { @Override public void onPersonChoose(Person person) { Toast.makeText( Situation2Activity.this, String.format( getString(R.string.sit02_summary_message), person.firstName, person.lastName), Toast.LENGTH_SHORT ).show(); } }); dialog.show(getSupportFragmentManager(), Situation2DialogFragment.class.getName()); } 

Such an implementation is resistant to any screen rotations. Everything will work like a clock. For the time being ...
We apply a sequence of actions for the cunning destruction process when the dialogue is open. When deployed, nothing will happen. However, when invokePersonChoose is called, the application will crash with a NullPointerException.

What happened?
setRetainInstance (true) did not allow the dialog to be destroyed. After the destruction of the process, the dialogue was nevertheless destroyed. Activity restores the snippet as much as possible. Unfortunately, the listener is not restored, as it was installed in a completely different place for a completely different object. When in a dialog, in the invokePersonChoose method, a call is made to the listener, an exception is thrown. And the trouble is not in the absence of checking for null. Putting a check on null without a proper reaction to an empty link would be an even worse solution.

What to do?
The Internet describes a bunch of ways to transfer messages from the fragment in the Activity. Unfortunately, not all of them are correct. The following method is correct and one of my favorites:

The only thing you should not forget is that Activity can already be destroyed when it comes to sending a message. Therefore, be sure to check whether the fragment is added to the Activity.
By passing Activity, you can use the parent fragment or Target fragment.

Just a couple of words about setRetainInstance
I note this is only a special case with setRetainInstance. The number of problems that he can hide (and not solve) a little more. Together with the listeners, all other variables are also lost. Anything that was not stored in the onSaveInstanceState method will be lost.
Also, it hides the problem when the dialog class is anonymous. Suppose, at the moment of creating a new dialog object, some method is redefined, in this case an object of an anonymous class will be created.
An example of creating an object with an anonymous class
 DialogFragment dialog = new DialogFragment() { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } }; 

If this dialog is destroyed and the system tries to restore it, a ClassNotFoundException exception is thrown.

Situation 3: Broken threads


Let the essence of the application be as follows:
On-screen button. When you click on the button, a progress dialog opens, blocking interaction with the UI.
Progress dialogue code
 public class ProgressDialogFragment extends DialogFragment { public static final String DISMISS_ACTION = "ru.kamisempai.ProgressDialogFragment.DISMISS_ACTION"; private boolean isWaitingForDismiss = false; private BroadcastReceiver mDismissActionReceiver; @Override public void onAttach(Activity activity) { super.onAttach(activity); mDismissActionReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { dismiss(); } }; LocalBroadcastManager.getInstance(activity) .registerReceiver(mDismissActionReceiver, new IntentFilter(DISMISS_ACTION)); } @Override public void onDetach() { if (mDismissActionReceiver != null) LocalBroadcastManager.getInstance(getActivity()) .unregisterReceiver(mDismissActionReceiver); super.onDetach(); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final android.app.ProgressDialog dialog = new android.app.ProgressDialog(getActivity()) { @Override public void onBackPressed() { // do nothing } }; dialog.setMessage("Please wait..."); dialog.setCancelable(false); dialog.setCanceledOnTouchOutside(false); return dialog; } @Override public void onResume() { super.onResume(); if(isWaitingForDismiss) dismiss(); } @Override public void dismiss() { if(isResumed()) super.dismiss(); else isWaitingForDismiss = true; } } 

As you can see from the code, this dialog can be closed by sending a broadcast message. This means that there is no need to keep a link to it, it is enough to have the application context.
Immediately after starting the dialogue, a thread is started that performs something for a while. At the very end of its work, the thread sends a message to close the progress dialog.
Creating and running a stream
 public void onStartActionClick(View view) { new ProgressDialogFragment() .show(getSupportFragmentManager(), ProgressDialogFragment.class.getName()); final Context context = getApplicationContext(); new Thread() { @Override public void run() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } LocalBroadcastManager.getInstance(context) .sendBroadcast(new Intent(ProgressDialogFragment.DISMISS_ACTION)); } }.start(); } 

If you run this code and without waiting for it to terminate the application, and then cut down the process, then when deploying nothing foreshadows trouble, the application does not crash, and the dialog is shown. We are waiting a little ... We are still waiting ... Then we are still waiting ... The dialogue does not close, although it should have been done for a long time.

What happened?
When a process is destroyed, all its threads are stopped, and during recovery only the main thread is started. So if your thread is running too long, be prepared for the fact that it will be stopped. And it is not necessary to do something for a long time, you can also step on this rake when using wait notify. It will be especially funny if the public static final Object is used as a blocking object, because we already know that static objects are no exception when the process is destroyed.
Example with wait notify
 private static final Object LOCK = new Object(); public void onStartWaitClick(View view) { // ... new Thread() { @Override public void run() { synchronized (LOCK) { try { //    . LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //    } }.start(); } public void onContinueClick(View view) { new Thread() { @Override public void run() { synchronized (LOCK) { LOCK.notify(); } } }.start(); } 

What to do?
If the task takes a long time to put it into a separate service and run it in a new process, plus bring out the foreground Notification, otherwise the process will still be killed. In the case of wait notify, everything is a little more complicated and depends on the specific situation. In general, the topic of working with threads is quite extensive, and it is inappropriate to give any specific advice here. Do not complicate and do not climb into the jungle, from which you can not get out.

Situation 4: Letter to Nowhere


There is an Activity with a single View. She is subscribed to receive broadcast messages. When an Activity receives such a message, the color of the View changes depending on the content of this message. When you click on the View, the second Activity opens, through which you can send the same messages to change the color.
First Activity Code
 public class Situation4FirstActivity extends BaseActivity { public static final String ACTION_SET_COLOR = "ru.kamisempai.Situation4FirstActivity.ACTION_SET_COLOR"; public static final String EXTRA_COLOR = "ru.kamisempai.Situation4FirstActivity.EXTRA_COLOR"; private View mView; private BroadcastReceiver mSetColorActionReceiver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mView = new View(this); setContentView(mView); mView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //   Activity startActivity(new Intent(Situation4FirstActivity.this, Situation4SecondActivity.class)); } }); mSetColorActionReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, final Intent intent) { //    view     runOnUiThread(new Runnable() { @Override public void run() { mView.setBackgroundColor(intent.getIntExtra(EXTRA_COLOR, Color.BLUE)); } }); } }; LocalBroadcastManager.getInstance(this) .registerReceiver(mSetColorActionReceiver, new IntentFilter(ACTION_SET_COLOR)); } @Override protected void onDestroy() { //   Activity       LocalBroadcastManager.getInstance(this) .unregisterReceiver(mSetColorActionReceiver); super.onDestroy(); } } 

In order for the first Activity not to be destroyed when the screen rotates, configChanges is added to the manifest file.
 <activity android:name=".situation_04.Situation4FirstActivity" android:configChanges="orientation|screenSize"/> 

In the second Activity there is a button, clicking on which sends a message to change the color.
Second Activity Code
 public class Situation4SecondActivity extends BaseActivity { private Random mRandom; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.sit04_activity_second); mRandom = new Random(System.currentTimeMillis()); } public void onRandomizeColorClick(View view) { Intent intent = new Intent(Situation4FirstActivity.ACTION_SET_COLOR); intent.putExtra( Situation4FirstActivity.EXTRA_COLOR, Color.BLACK + mRandom.nextInt(0xFFFFFF)); LocalBroadcastManager.getInstance(this).sendBroadcast(intent); } } 

When returning to the first Activity, the View color will be changed.

However, if you switch to the second Activity, minimize the application, kill the process, and then deploy the application, something unforeseen happens. No matter how many times you press the color change button, when you return to the first Activity, its View will be pure white.

What happened?
Despite the fact that the stack of activities has been preserved, after the application was deployed, only the second Activity was restored. In fact, the color change messages went nowhere. Since at that time there were no objects subscribed to this event. Upon returning to the first Activity, it was restored, but it was too late to subscribe to the color change event.

What to do?
First of all, you need to understand the main thing - if you work with one Activity, the others do not exist for it. If you need to convey any information, use the intended setResult. Do not blindly rely on life cycles. As you can see from the example, if Activity 1 launched Activity 2, this does not mean that the onCreate method was the first Activity to be executed.
Also note, this example shows not only the problems with the stack of activities, but also problems with everything related to sending messages. The only exceptions are BroadcastReceivers specified in the manifest or scheduled via AlarmManager. Everything else does not give a 100% guarantee for the delivery of the message to the addressee.

Conclusion


For experienced coders, these situations may seem obvious, and examples are not so natural as to simply turn back. However, we all once started from scratch and wrote code from which now would be a shame. If I had known about these rakes at the very beginning of my journey, there would be fewer cones on my forehead. I hope this article will help guide the true large number of beginner Android programmers on the way. From “Experienced” I will be glad to see the comments. And even better if you add to the list or write, did this article help catch bugs from the category “How ?! It can not be!".

On this finish. Thanks for attention. Finally, I note that I still have one extraordinary case in my sleeve. It has not been studied by me to the end, but I hope to soon split this tough nut. So wait for the continuation.

Source: https://habr.com/ru/post/261721/


All Articles