Google Play 控制台报告应用程序崩溃,我无法重现
Posted
技术标签:
【中文标题】Google Play 控制台报告应用程序崩溃,我无法重现【英文标题】:Google Play console reports app crash, I can not reproduce 【发布时间】:2019-04-01 08:23:03 【问题描述】:android Vitals 报告中的 Google Play 控制台为我提供了此信息
java.lang.RuntimeException:
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:2955)
at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:3030)
at android.app.ActivityThread.-wrap11 (Unknown Source)
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1696)
at android.os.Handler.dispatchMessage (Handler.java:105)
at android.os.Looper.loop (Looper.java:164)
at android.app.ActivityThread.main (ActivityThread.java:6938)
at java.lang.reflect.Method.invoke (Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run (Zygote.java:327)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1374)
Caused by: java.lang.NullPointerException:
at com.golendukhin.whitenoise.GridAdapter.getCount (GridAdapter.java:52)
at android.widget.GridView.setAdapter (GridView.java:243)
at com.golendukhin.whitenoise.FoldersGridActivity.onCreate (FoldersGridActivity.java:60)
at android.app.Activity.performCreate (Activity.java:7183)
at android.app.Instrumentation.callActivityOnCreate (Instrumentation.java:1220)
at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:2908)
据我了解,这是因为网格适配器的 getCount 方法返回空值。 Hense,网格适配器不起作用。因此,没有创造出全部的热情。只有三星设备存在这样的问题。
首先,我尝试创建具有相同 Android 版本、RAM 和 ARM 的相同模拟设备。模拟器工作得非常好。没有错误。
然后我添加了一些测试以确保 getCount 工作并创建活动。
@RunWith(AndroidJUnit4.class)
public class GridViewTest
@Rule
public ActivityTestRule<FoldersGridActivity> foldersGridActivityTestRule = new ActivityTestRule<>(FoldersGridActivity.class);
@Test
public void ensureGridAdapterIsNotNull()
FoldersGridActivity foldersGridActivity = foldersGridActivityTestRule.getActivity();
View view = foldersGridActivity.findViewById(R.id.grid_view);
GridView gridView = (GridView) view;
GridAdapter gridAdapter = (GridAdapter) gridView.getAdapter();
assertThat(gridAdapter, notNullValue());
@Test
public void ensureArrayAdapterInGridActivityIsNotEmpty()
FoldersGridActivity foldersGridActivity = foldersGridActivityTestRule.getActivity();
View view = foldersGridActivity.findViewById(R.id.grid_view);
GridView gridView = (GridView) view;
GridAdapter gridAdapter = (GridAdapter) gridView.getAdapter();
assertTrue(gridAdapter.getCount() > 0 );
@Test
public void gridViewIsVisibleAndClickable()
onData(anything()).inAdapterView(withId(R.id.grid_view)).atPosition(0).perform(click());
onView(withId(R.id.toolbar_title_text_view)).check(matches(withText("rain")));
所有模拟设备再次成功通过测试。 然后我添加了 FireBase TestLab 并在装有 Android 8 的 Galaxy S9 真机上启动了我的测试。所有测试都通过了。
适配器代码。是的,我已经覆盖了 getCount() 方法,即使它没有意义。
public class GridAdapter extends ArrayAdapter<TracksFolder>
@BindView(R.id.folder_text_view) TextView textView;
@BindView(R.id.grid_item_image_view) ImageView imageView;
private ArrayList<TracksFolder> tracksFolders;
private Context context;
GridAdapter(Context context, int resource, ArrayList<TracksFolder> tracksFolders)
super(context, resource, tracksFolders);
this.tracksFolders = tracksFolders;
this.context = context;
/**
* @return grid item, customized via TracksFolder class instance
*/
@Override
public View getView(int position, View convertView, ViewGroup parent)
if (convertView == null)
convertView = LayoutInflater.from(context).inflate(R.layout.grid_item, parent, false);
ButterKnife.bind(this, convertView);
String folderName = tracksFolders.get(position).getFolderName();
int imageResource = tracksFolders.get(position).getImageResource();
textView.setText(folderName);
imageView.setImageResource(imageResource);
return convertView;
/**
* Added to possibly avoid NPE on multiple devices
* Could not reproduce this error
* @return size of tracksFolders
*/
@Override
public int getCount()
return tracksFolders.size();
所以,我有几个问题: 首先,这个崩溃报告是否意味着每次 Sumsung 所有者启动应用程序时都会发生这种崩溃?由于一些三星设备坚持统计,我认为不会。 其次,我应该怎么做才能以某种方式重现这个错误并最终修复这个错误。
我知道问题太宽泛了,但我非常感谢任何帮助,因为我完全不知道下一步该做什么。
入门级。使用适配器
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.GridView;
import com.google.firebase.crash.FirebaseCrash;
import java.util.ArrayList;
import butterknife.BindArray;
import butterknife.BindView;
import butterknife.ButterKnife;
import static com.golendukhin.whitenoise.SharedPreferencesUtils.writeToSharedPreferences;
public class FoldersGridActivity extends AppCompatActivity
@BindView(R.id.grid_view) GridView gridView;
@BindArray(R.array.labels) String[] foldersName;
@BindView(R.id.toolbar_like_button) Button toolbarLikeButton;
@BindView(R.id.toolbar_back_button) Button toolbarBackButton;
private ArrayList<TracksFolder> tracksFolders;
/**
* Binds all UI views
* Defines variables to feed adapter and listeners
* Initializes grid adapter, sets number of columns via phone in portrait or landscape orientation
* Sets onclick listener for every grid item
* Sets onclick for toolbar "like" button
*/
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.folders_grid_view);
ButterKnife.bind(this);
if (getIntent().getExtras() != null) //activity is triggered via on back pressed
Bundle bundle = getIntent().getExtras();
tracksFolders = (ArrayList<TracksFolder>) bundle.getSerializable("tracksFolders");
else if (savedInstanceState != null) //this happens after screen is rotated
tracksFolders = (ArrayList<TracksFolder>) savedInstanceState.getSerializable("tracksFolders");
else //this happens if app is launched by user
tracksFolders = getTracksFolders();
initService();
toolbarBackButton.setVisibility(View.GONE);
GridAdapter gridAdapter;
gridAdapter = new GridAdapter(this, R.id.grid_view, tracksFolders);
gridView.setNumColumns(getResources().getInteger(R.integer.columns_number));
gridView.setAdapter(gridAdapter);
FirebaseCrash.log("Activity created");
gridView.setOnItemClickListener(new AdapterView.OnItemClickListener()
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
Bundle bundle = new Bundle();
bundle.putSerializable("tracksFolders", tracksFolders);
bundle.putInt("folderPosition", position);
Intent intent = new Intent(FoldersGridActivity.this, TracksListActivity.class);
intent.putExtras(bundle);
startActivity(intent);
);
toolbarLikeButton.setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View view)
Bundle bundle = new Bundle();
bundle.putSerializable("tracksFolders", tracksFolders);
bundle.putInt("activityFrom", Constants.FROM_FOLDERS_GRID_ACTIVITY);
Intent intent = new Intent(FoldersGridActivity.this, LikedTracksListActivity.class);
intent.putExtras(bundle);
startActivity(intent);
);
/**
* Saves tracksFolders array list to rebuild activity after rotation
*/
@Override
protected void onSaveInstanceState(Bundle outState)
outState.putSerializable("tracksFolders", tracksFolders);
writeToSharedPreferences(this, tracksFolders);
super.onSaveInstanceState(outState);
@Override
public void onBackPressed()
super.onBackPressed();
moveTaskToBack(true);
/**
* After app is launched need to initialize audio player service
*/
private void initService()
Intent intent = new Intent(FoldersGridActivity.this, AudioPlayerService.class);
startService(intent);
/**
* If tracksFolders is not stored in shared preferences, it is initialized from resources.
* @return arrayList of folders with arrayList of tracks belonging to every folder
*/
private ArrayList<TracksFolder> getTracksFolders()
Resources resources = getResources();
TypedArray pictures = resources.obtainTypedArray(R.array.pictures);
ArrayList<TracksFolder> tracksFolders = new ArrayList<>();
for (int i = 0; i < foldersName.length; i++)
String folderName = foldersName[i];
int imageResource = pictures.getResourceId(i, -1);
ArrayList<Track> tracks = getArrayListOfTracks(resources, i, imageResource);
tracksFolders.add(new TracksFolder(folderName, imageResource, tracks));
pictures.recycle();
return tracksFolders;
/**
* @param resources need to get typedArray to get array of audio files names
* @param folderPosition passed to Track constructor
* @param imageResource passed to Track constructor
* Liked tracks are obtained from shared preferences
* @return array list of tracks for every folder
*/
private ArrayList<Track> getArrayListOfTracks(Resources resources, int folderPosition, int imageResource)
TypedArray audioTracksTypedArray = resources.obtainTypedArray(R.array.audio_tracks);
int audioTracksId = audioTracksTypedArray.getResourceId(folderPosition, 0);
String[] audio = resources.getStringArray(audioTracksId);
audioTracksTypedArray.recycle();
TypedArray trackNamesTypedArray = resources.obtainTypedArray(R.array.track_names);
int trackNamesId = trackNamesTypedArray.getResourceId(folderPosition, 0);
String[] trackNames = resources.getStringArray(trackNamesId);
trackNamesTypedArray.recycle();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
ArrayList<Track> tracksInFolder = new ArrayList<>();
for (int i = 0; i < trackNames.length; i++)
int audioResource = resources.getIdentifier(audio[i], "raw", this.getPackageName());
String trackName = trackNames[i].replace("_", " ");
boolean isLiked = sharedPreferences.getBoolean(String.valueOf(audioResource), false);
tracksInFolder.add(new Track(audioResource, trackName, imageResource, isLiked));
return tracksInFolder;
//todo убрать полосы в списках треков
更新类:
@NonNull
private ArrayList<TracksFolder> getTracksFoldersAndInitService(Bundle savedInstanceState)
if (getIntent().getExtras() != null) //activity is triggered via on back pressed
Bundle bundle = getIntent().getExtras();
tracksFolders = (ArrayList<TracksFolder>) bundle.getSerializable("tracksFolders");
else if (savedInstanceState != null) //this happens after screen is rotated
tracksFolders = (ArrayList<TracksFolder>) savedInstanceState.getSerializable("tracksFolders");
else //this happens if app is launched by user
tracksFolders = getTracksFoldersFromResources();
initService();
return tracksFolders;
@Nick Fortescue、@Ranjan,感谢你们的建议,但 trackFolders 永远不会为 NULL。我已经编辑了类(您可以在“更新的类:”下面看到它)并将tracksFolders实例化到另一个带有@NonNull注释的方法中。我已经使用此更改更新了应用程序,后来获得了具有相同日志的相同 NPE。我知道这是在用户身上测试应用程序的最糟糕的方式,但我别无选择。如果你是对的,日志会说在 getTracksFoldersAndInitService 方法中抛出了异常。但它没有。因此,问题出在适配器上。如果我错了,请纠正我。
【问题讨论】:
很可能您的 GridAdapter 正在使用 trackFolders 的空数组列表进行实例化。您可以在实例化 GridAdapter 的位置发布您的 Activity 代码吗? @Ranjan,是的,这听起来像是可能的原因。我添加了使用适配器的 FoldersGridActivity 您的第三个条件实例化了数组列表。这意味着如果前两个中的任何一个为真,则 arraylist 来自其他地方,其中一个将其设置为 null。您应该尝试调试前两个条件。这也可能是制造商特定的问题。您也可以在定义时实例化列表,这样它至少不会崩溃。private ArrayList<TracksFolder> tracksFolders = new ArrayList<>();
实际上在定义时实例化并不重要,因为这两个请求中的任何一个都会将其设置回 null。
【参考方案1】:
GridAdapter.getCount()
中发生的 NullPointerException 并不意味着 getCount() 返回 null。这意味着它正在访问一个空值。由于您上传了代码(太棒了!),我们可以看到只有两个可能的问题:
trackFolders
是 null
,所以当你访问它时,你会得到一个 NPE
trackFolders
返回一个 null
整数,因此当它转换为 int
时,您会得到一个 NPE
第二个选项不可能,因为ArrayList
上的.size()
也返回一个int,所以它必须是第一个选项。
trackFolders
是在构造函数中设置的,它是在你的 onCreate()
方法中从包、保存状态或 getTracksFolders()
中设置的。看起来getTracksFolders()
不能返回null,所以我猜你的活动是从这个设置为null的意图开始的。我会在您的 onCreate() 方法中仔细检查这一点,如果发生,请采取适当的措施,也许将其替换为 new ArrayList()
?
【讨论】:
以上是关于Google Play 控制台报告应用程序崩溃,我无法重现的主要内容,如果未能解决你的问题,请参考以下文章