【发布时间】:2015-04-05 15:58:04
【问题描述】:
我是 Android 和 Java 开发的新手,我整理了一个简单的演示应用来开始学习。它是由:
- 扩展
ActionBarActivity的主要活动,其中 -
ViewPager被实例化,它有 - 一个
FragmentPagerAdapter负责出现... - ...
Fragment在给定时间三分之二,并且 - 其中一个片段在创建时,只是为了尝试一下,执行一个
AsyncTask(由另一个类定义),它在完成时触发一个HTTP request(onPostExecute)... - 在触发它的 Fragment 中填充
TableLayout。
我也在努力保持与旧版 Android 平台的兼容性,因此我在必要时使用了支持库。
我看到的问题,或者我应该说我看到(继续阅读..),是不时强>我明白了
org.apache.http.conn.HttpHostConnectException:连接到http://123.123.123.123:80/notes.json被拒绝
在大约 40 秒的长时间等待后出现超时错误消息。 当然已经添加了 Internet 权限。
这是随机发生的,通常在垃圾收集器首次运行后发生。
花了几天的时间尝试调试后,我终于重新启动了系统,这种行为完全消失了。
但是,考虑到我花了这么多时间(认为我严重泄露了一些东西),我仍然想:
- 了解发生了什么
- 了解我是否正确分析内存泄漏
1:了解发生了什么:
启动应用程序时显示的屏幕是调用负责更新片段 UI 本身的 AsyncTask 的片段(它被分配给第一个操作栏选项卡)。
应用一启动,我就开始不断旋转屏幕,看看内存会发生什么变化。下面的截图来自 Android Studio 内存分析器。
第一次连接超时错误通常发生在第一次运行垃圾收集器之后,在我通过多次旋转设备填满所有可用内存之后。
此时,AsyncTask 更新片段 UI 失败(在处理显然无响应的连接时卡住了)。 Web 服务器根本不会收到 HTTP 请求,即使我再次旋转屏幕——为了让 Activity 和 Fragment 重新启动——后续的 AsyncTask 也不起作用,也不会发出新的 HTTP 请求。
当然,我一直在捕获所有异常,并且在 onPostExecute() 开始时,我必须执行if (arrayOfJSONobjs == null) { return; } 以避免将空对象输入到后续片段 UI 的构建方法中。
那么,您认为会发生什么事情才能使连接以这样的方式工作?重启后我怎么看不到了? 我已尝试禁用防病毒软件、防火墙,并检查路由器或网络服务器是否为连续请求过多应用某种保护。(我的设备连接到网络服务器来自互联网,使用我的公共 IP)。没有任何效果,除了重新启动。我唯一剩下的想法是.. 可能Android Studio 中的某个错误有时会在请求中间出现?
2:我是否正确理解了内存分配和 GC?
查看代码,您认为在某些地方我可能会泄漏上下文吗? 我在内存分析器屏幕截图中看到的是非泄漏应用程序的预期良好行为吗?我的意思是,即使没有泄漏,我是否应该看到内存被填满(前提是它会被垃圾收集)?
我不知道如何更好地表达这一点,但是当一切顺利时,我是否应该看到这种图表?如您所见,第一次 GC 仅在内存完全填满时调用,但随后 GC 触发更快,此时仍有一些可用内存(尽管我仍在旋转设备)。这是正常吗?
尽管存在上述错误(但仍可能与它们相关,以防内存泄漏实际发生):我不确定是否必须将视图和上下文都传递给 AsyncTask 对象。我可以通过只有其中一个并从中推断出另一个,以便尽可能减少我传递的参考?
清理:关于 TableLayout 是否适合我正在尝试构建的布局的子问题已移至 another question。
代码:
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(负责实例化和启动 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;
}
}
}
}
【问题讨论】:
-
好吧,我在 HTTP 代码中看不到任何问题。可能是您超出了 Apache 的
DefaultHttpClient或其他地方允许的并发连接的最大限制。您应该将您的 HTTP 客户端实现切换到HttpUrlConnection,因为这是所有开发的重点。更好的是使用OkHttp library,这在很多地方都是一个改进,实际上是从 KitKat 4.4 开始支持HttpUrlConnection。 -
这里唯一的内存泄漏是
AsyncTask在Activity重启后保留的过时Context的临时强引用。您应该重新实现AsyncTask以松散耦合到视图层,这也将允许它被重用而不是每次都重新启动。您的选择是:1) 使用AsyncTaskLoader(复杂,但语义正确)2) 为其提供对保留Fragment的引用,这将始终提供最新的Activity实例引用。 3) 使用事件总线通知视图层,而不是持有对它的引用。 -
关于您的代码问题:所有视图都包含对
Context的引用,可以从getContext()方法中检索到,因此您无需将显式引用作为参数传递与View。TableLayout适用于表格布局。GridLayout是另一个更轻量级的替代方案,尽管它不支持权重系统。 -
垃圾收集除了会在
OutOfMemoryError被抛出之前执行之外没有任何保证。实际的启发式方法取决于实现。 Gingerbread 引入了并发和部分垃圾收集器,显示的行为似乎与此一致。无论如何,我看不出垃圾回收会如何影响 HTTP 连接。 -
谢谢,为什么不把这个放在答案中呢?它可能不完整,但现在它肯定值得一些 +1 :)
标签: android memory-leaks android-asynctask android-studio android-context