I'm new at Android and Java development I've put together a simple demo app to start learning. It is made of:
- a main activity extending
ActionBarActivity
, in which - a
ViewPager
is instantiated which has - a
FragmentPagerAdapter
responsible for showing up... - ...one
Fragment
out of three at a given time and - one of those fragments, when it is created, just to try things out, executes an
AsyncTask
(defined by another class) which triggers anHTTP request
that when done (onPostExecute)... - populates a
TableLayout
in the Fragment that fired it.
I'm also trying to keep compatibility with older Android platforms, so I'm using support library where necessary.
The problem that I am seeing, or I should say that I was seeing (keep reading..), is that from time to time I get
org.apache.http.conn.HttpHostConnectException: Connection to http://123.123.123.123:80/notes.json refused
together with a timeout error message after a long wait of aproximately 40 seconds. Of course Internet permission has already been added.
This happens randomly and it usually does after first run of Garbage Collector.
After spending days trying to debug it, I finally did a system reboot and this behaviour totally disappeared.
But, given I spent so much time on it (thinking that I was badly leaking something), I would still like to:
- Understand what was going on
- Understand if I am correctly profiling memory leaks
1: Understand what was going on:
The screen that shows up when starting the app, is the fragment that calls the AsyncTask responsible of updating the fragment UI itself (it is assigned to the first Action Bar tab).
As soon as the app started I began to continuosly rotate the screen to see what would happen to the memory. The screenshot below is from Android Studio memory profiler.
The first connection-timeout error usually occurred right after first run of Garbage Collector, after I had filled up all available memory by rotating the device many times.
At this point it happened that the fragment UI failed at being updated by AsyncTask (which was stuck processing the apparently unresponsive connection). The web server would not receive the HTTP request at all, and even if I rotated the screen again - in order for the Activity and Fragment to restart - subsequent AsyncTask's did not work too and no new HTTP requests would be made.
Of course I've been catching all exceptions and, at the beginning of onPostExecute() I had to do if (arrayOfJSONobjs == null) { return; }
in order to avoid feeding in a null object to the subsequent fragment UI's building methods.
So, what do you think could have happened to make the connection work like that? How is that that after reboot I am not seeing that again? I've tried disabling antivirus, firewall, and checked if router or web server were applying some kind of protection for too many consecutive requests. (My device connects to the web server from the internet, using my public IP). Nothing worked, except reboot. The only thing I'm left with thinking is.. possibly some bug in Android Studio which got in the middle of the requests at some time?
2: Am I correctly understanding memory allocation and GC?
Looking at the code, do you think there is some place where I could possibly be leaking the context? Is what I see in the memory profiler screenshot the expected good behaviour for a non leaking app? I mean, am I supposed to see memory being filled up even when there is no leaking (provided that then it gets garbage collected) ?
I don't know how to better put this, but am I expected to see this kind of graph when everything is going ok? As you can see, the first time, the GC is only invoked when memory is completely filled up, but afterwards GC triggers in sooner, when there is still some memory available (I was still rotating the device though). Is this normal?
Despite the errors above (but still possibly related to them, in case memory leaking is actually happening): I am unsure about having to pass both the view and the context to the AsyncTask object. Could I just pass only one of those and infer the other one from it, in order to minimize as much as possible the references I am passing?
Cleaning up: the subquestion about whether or not TableLayout is a good fit for the layout I'm trying to build has been moved to another question.
Code:
MainActivity.java
package com.mydom.demoapp;
import android.os.Bundle;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.NavUtils;
import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import com.mydom.demoapp.adapter.TabsPagerAdapter;
public class MainActivity extends ActionBarActivity implements ActionBar.TabListener {
private ViewPager viewPager;
private TabsPagerAdapter mAdapter;
private ActionBar actionBar;
private String[] tabs = { "Music", "Movies", "News"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewPager = (ViewPager) findViewById(R.id.pager);
mAdapter = new TabsPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(mAdapter);
actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setHomeButtonEnabled(true);
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
// Adding Tabs
for (String tab_name : tabs) {
actionBar.addTab(actionBar.newTab().setText(tab_name).setTabListener(this));
}
// on swiping the viewpager make respective tab selected
viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageSelected(int position) {
actionBar.setSelectedNavigationItem(position);
}
@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}
@Override
public void onPageScrollStateChanged(int arg0) {
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here
int id = item.getItemId();
if (id == R.id.action_settings) {
return true;
}
if (item.getItemId() == android.R.id.home) {
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {
}
@Override
public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
// on tab selected show appropriate fragment
viewPager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) {
}
@Override
public void onStop(){
super.onStop();
Log.d("mytag", "MainActivity: onStop entered");
}
}
TabsPageAdapter.java
package com.mydom.demoapp.adapter;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import com.mydom.demoapp.MusicFragment;
import com.mydom.demoapp.MoviesFragment;
import com.mydom.demoapp.NewsFragment;
// This adapter provides fragment views to tabs.
public class TabsPagerAdapter extends FragmentPagerAdapter{
public TabsPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int index) {
switch (index) {
case 0:
return new MusicFragment();
case 1:
return new MoviesFragment();
case 2:
return new NewsFragment();
}
return null;
}
@Override
public int getCount() {
// get item count - equal to number of tabs
return 3;
}
}
MusicFragment.java (responsible for instantiating and launching an AsyncTask)
package com.mydom.demoapp;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.mydom.demoapp.async_task.AsyncTaskRunner;
public class MusicFragment extends Fragment {
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
private String mParam1;
private String mParam2;
private OnFragmentInteractionListener mListener;
public static MusicFragment newInstance(String param1, String param2) {
MusicFragment fragment = new MusicFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
public MusicFragment() {
// Required empty public constructor
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
Log.d("mytag", "MusicFragment: onCreate entered");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Inflate the layout for this fragment
Log.d("janfry", "MusicFragment: onCreateView entered");
return inflater.inflate(R.layout.fragment_music, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Log.d("mytag", "MusicFragment: onViewCreated entered");
runner = new AsyncTaskRunner();
runner.execute(this.getActivity(), view);
// I am passing it the context (by getting the activity) and the view so that it will know where to update the UI.
}
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
// TODO: Rename method, update argument and hook method into UI event
public void onButtonPressed(Uri uri) {
if (mListener != null) {
mListener.onFragmentInteraction(uri);
}
}
public interface OnFragmentInteractionListener {
// TODO: Update argument type and name
public void onFragmentInteraction(Uri uri);
}
@Override
public void onStop(){
super.onStop();
Log.d("mytag", "MusicFragment: onStop entered");
}
}
AsyncTaskRunner.java
package com.mydom.demoapp.async_task;
import android.content.Context;
import android.graphics.Color;
import android.os.AsyncTask;
import android.util.Log;
import android.view.View;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import com.mydom.demoapp.R;
import com.mydom.demoapp.Utils;
import org.apache.http.HttpResponse;
// import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
// import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
// import org.json.JSONException;
import org.json.JSONObject;
// import java.io.IOException;
// import java.net.SocketTimeoutException;
// import java.util.concurrent.TimeoutException;
public class AsyncTaskRunner extends AsyncTask<Object, String, JSONArray> {
// These will be set by doInBackground() according to what the fragment passed to it
// I am declaring them as instance variables because I'll need them in the onPostExecute method too, so to have a ref to the frag to update.
// By the way, can I infer one from the other someway?
Context contextRef;
View viewRef;
@Override
protected void onPreExecute(){
Log.d("janfry", "AsyncTaskRunner: onPreExecute entered");
}
@Override
protected JSONArray doInBackground(Object... params){
Log.d("mytag", "AsyncTaskRunner: doInBackground entered");
contextRef = (Context) params[0];
viewRef = (View) params[1];
HttpResponse response;
String str = "";
JSONArray arrayOfJSONObjects = null;
final HttpParams httpParams = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
HttpClient myClient = new DefaultHttpClient(httpParams);
HttpGet myConnection = new HttpGet("http://123.123.123.123:80/notes.json");
try {
response = myClient.execute(myConnection);
str = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Throwable t) {
t.printStackTrace();
}
try{
arrayOfJSONObjects = new JSONArray(str);
} catch ( Throwable t) { t.printStackTrace(); }
try {
Log.d("mytag", arrayOfJSONObjects.getString(0));
} catch (Throwable t) {
t.printStackTrace();
}
return arrayOfJSONObjects;
}
@Override
protected void onProgressUpdate(String... notused){
}
@Override
protected void onPostExecute(JSONArray arrayOfJSONobjs) {
Log.d("mytag", "AsyncTaskRunner: onPostExecute entered");
TableLayout tab_lay = (TableLayout) viewRef.findViewById(R.id.musicTableLayout);
tab_lay.removeAllViews();
TextView[] arrayOfTextViews;
arrayOfTextViews = new TextView[arrayOfJSONobjs.length()];
for(int pos = 0; pos < arrayOfJSONobjs.length(); pos++) {
// and let's populate it with textviews...
TextView textViewForObjName = new TextView(contextRef);
try {
JSONObject oneJsonObj; // will hold the parsed JSON for one obj
oneJsonObj = arrayOfJSONobjs.getJSONObject(pos);
textViewForObjName.setText(oneJsonObj.getString("name"));
} catch (Throwable t) {
t.printStackTrace();
}
textViewForObjName.setHeight(Utils.dip(contextRef, 30));
textViewForObjName.setBackgroundColor(Color.parseColor("#ABCABC"));
// let's add the text_view we built to the Array
arrayOfTextViews[pos] = textViewForObjName;
} // we now have an array of textviews, that has not been added to the UI yet.
// I want to populate the array with 3 textviews per row.
// Is it a good idea to use this layout for this way of laying out content?
// Would you have done that differently?
TableRow table_row = new TableRow(contextRef);
int col_counter = 0;
for (TextView aTextView : arrayOfTextViews) {
table_row.addView(aTextView);
col_counter++;
if (col_counter == 3) {
tab_lay.addView(table_row);
table_row = new TableRow(contextRef);
col_counter = 0;
}
}
}
}