1

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 an HTTP 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:

  1. Understand what was going on
  2. 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?

enter image description here

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;
            }
        }

    }


}
Community
  • 1
  • 1
Redoman
  • 3,059
  • 3
  • 34
  • 62
  • 1
    Well, I don't see any issue in the HTTP code. It may be that you were exceeding some maximum limit of concurrent connections allowed in Apache's `DefaultHttpClient` or elsewhere. You should switch your HTTP client implementation to `HttpUrlConnection`, as that is where all the development was focused on. Even better would be to use the [OkHttp library](http://square.github.io/okhttp/), which is an improvement in a lot of places, and in fact is what backs up `HttpUrlConnection` as of KitKat 4.4. – corsair992 Feb 05 '15 at 19:38
  • 1
    The only memory leaking here is the temporary strong reference of the obsolete `Context` that the `AsyncTask` retains after the `Activity` is restarted. You should re-implement the `AsyncTask` to be loosely coupled to the view layer, which will also allow it to be reused instead of restarted each time. Your options are: 1) Use an `AsyncTaskLoader` (complex, but correct semantics) 2) Provide it a reference to a retained `Fragment`, which will always provide the up-to-date `Activity` instance reference. 3) Use an event bus for notifying the view layer, instead of holding a reference to it. – corsair992 Feb 05 '15 at 19:39
  • Re your code questions: All views contain a reference to a `Context`, which can be retrieved from the `getContext()` method, so you don't need to pass an explicit reference as an argument along with the `View`. `TableLayout` is fine for table layouts. `GridLayout` is another more light-weight alternative, although it does not have support for the weight system. – corsair992 Feb 05 '15 at 19:39
  • Garbage collection has no guarantees other than that it will be performed before an `OutOfMemoryError` is thrown. The actual heuristics are implementation-dependent. Gingerbread introduced a concurrent and partial garbage collector, and the displayed behavior seems to be in accordance with that. In any case, I don't see how garbage collection can affect an HTTP connection. – corsair992 Feb 05 '15 at 19:59
  • Thanks, why not put this in an answer? It might not be complete, but for now it surely it deserve some +1's :) – Redoman Feb 07 '15 at 02:44
  • This question is too broad really, as it contains lots of mini questions that should preferably have been asked separately. The 'overview and advice` section belongs in the Code Review site. I don't have a real answer to the main question here, but I posted some general comments so you might get some clarity on the issues. – corsair992 Feb 07 '15 at 02:58
  • @corsair992 I had actually posted it at Code Review too http://codereview.stackexchange.com/q/79633/64442 . In there I kept only the code review part, and here I'm focusing more on the AsyncTask HTTP errors. My initial thought was that those errors were caused by memory leaking, that's why I've also kept those subquestions here. Your `It may be that you were exceeding some maximum limit of concurrent connections allowed in Apache's DefaultHttpClient` is actually a possible answer to my main question, so please put it in an answer so it's more visible and could further help resolving the case. – Redoman Feb 07 '15 at 05:30
  • My suggestion of the limit on concurrent connections was just a guess. I don't have much experience with Apache's HTTP client, so I have no idea whether that's true or not. I just presented the possibility so you might consider switching to one of the better alternatives. – corsair992 Feb 07 '15 at 14:17

1 Answers1

0

@corsair992 is suggesting (it's just a guess) that "it may be that you were exceeding some maximum limit of concurrent connections allowed in Apache's DefaultHttpClient or elsewhere."

I advised her/him to put the above comment in a proper answer (together with other observations s/he did), but s/he said that it's just a guess and not a complete answer.

Still to me it's the most reasonable hypothesis and I think it deserves full visibility and attention so I am saving it as an answer myself so that maybe it can be further commented and expanded (please upvote the original @corsair992 comment under my question).

Redoman
  • 3,059
  • 3
  • 34
  • 62