首页 > 解决方案 > Google Play 控制台报告应用程序崩溃,我无法重现

问题描述

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 方法中抛出了异常。但它没有。因此,问题出在适配器上。如我错了请纠正我。

标签: androidfirebasetestinggridviewgoogle-play-console

解决方案


发生的 NullPointerExceptionGridAdapter.getCount()并不意味着 getCount() 返回 null。这意味着它正在访问一个空值。由于您上传了代码(太棒了!),我们可以看到只有两个可能的问题:

  • trackFoldersnull,所以当你访问它时,你会得到一个 NPE
  • trackFolders返回一个null整数,所以当它被转换为一个int你得到一个 NPE

第二个选项是不可能的,因为.size()on anArrayList也返回一个 int,所以它必须是第一个选项。

trackFolders在构造函数中设置,该构造函数在您的onCreate()方法中从捆绑包、已保存状态或从getTracksFolders(). 看起来getTracksFolders()不能返回 null,所以我猜你的活动是从一个设置为 null 的意图开始的。我会在您的 onCreate() 方法中仔细检查这一点,如果发生,请采取适当的措施,也许将其替换为new ArrayList()?


推荐阅读