วิธีป้องกันหลายอินสแตนซ์ของกิจกรรมเมื่อเปิดตัวด้วย Intent ที่แตกต่างกัน


121

ฉันพบข้อบกพร่องในแอปพลิเคชันของฉันเมื่อเปิดตัวโดยใช้ปุ่ม"เปิด"บนแอป Google Play Store (ก่อนหน้านี้เรียกว่า Android Market) ดูเหมือนว่าการเปิดตัวจาก Play Store จะใช้วิธีที่แตกต่างIntentจากการเปิดใช้งานจากเมนูไอคอนแอปพลิเคชันของโทรศัพท์ ซึ่งนำไปสู่การเปิดตัวกิจกรรมเดียวกันหลายชุดซึ่งขัดแย้งกัน

ตัวอย่างเช่นหากแอปของฉันประกอบด้วยกิจกรรม ABC ปัญหานี้อาจนำไปสู่ ​​ABCA หลายชุด

ฉันพยายามใช้android:launchMode="singleTask"กับกิจกรรมทั้งหมดเพื่อแก้ไขปัญหานี้ แต่มีผลข้างเคียงที่ไม่ต้องการจากการล้างสแต็กกิจกรรมไปยังรูทเมื่อใดก็ตามที่ฉันกดปุ่ม HOME

พฤติกรรมที่คาดไว้คือ: ABC -> HOME -> และเมื่อแอปได้รับการกู้คืนฉันต้องการ: ABC -> HOME -> ABC

มีวิธีที่ดีในการป้องกันการเรียกใช้กิจกรรมประเภทเดียวกันหลายรายการโดยไม่ต้องรีเซ็ตเป็นกิจกรรมรูทเมื่อใช้ปุ่ม HOME หรือไม่?


ตั๋วที่เกี่ยวข้องในเครื่องมือติดตามข้อบกพร่องของ Android: issueetracker.google.com/issues/36941942 , issueetracker.google.com/issues/36907463 , Issetracker.google.com/issues/64108432
Mr-IDE

คำตอบ:


187

เพิ่มสิ่งนี้ใน onCreate และคุณควรจะไป:

// Possible work around for market launches. See https://issuetracker.google.com/issues/36907463
// for more details. Essentially, the market launches the main activity on top of other activities.
// we never want this to happen. Instead, we check if we are the root and if not, we finish.
if (!isTaskRoot()) {
    final Intent intent = getIntent();
    if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(intent.getAction())) {
        Log.w(LOG_TAG, "Main Activity is not the root.  Finishing Main Activity instead of launching.");
        finish();
        return;       
    }
}

25
ฉันพยายามแก้ไขข้อบกพร่องนี้มาหลายปีแล้วและนี่เป็นวิธีแก้ปัญหาที่ได้ผลดังนั้นขอบคุณมาก! ฉันต้องทราบด้วยว่านี่ไม่ใช่แค่ปัญหาใน Android Market แต่ยังรวมถึงการโหลดแอปด้วยการอัปโหลดไปยังเซิร์ฟเวอร์หรือส่งอีเมลไปยังโทรศัพท์ของคุณทำให้เกิดปัญหานี้ ทุกสิ่งเหล่านี้ติดตั้งแอปโดยใช้ Package Installer ซึ่งฉันเชื่อว่ามีข้อบกพร่องอยู่ นอกจากนี้ในกรณีที่ไม่ชัดเจนคุณจะต้องเพิ่มโค้ดนี้ในเมธอด onCreate ว่ากิจกรรมรูทของคุณคืออะไร
ubzack

2
ฉันคิดว่ามันแปลกมากที่สิ่งนี้เกิดขึ้นในแอพที่ลงนามที่ปรับใช้กับอุปกรณ์ แต่ไม่ใช่เวอร์ชันดีบักที่ปรับใช้จาก Eclipse ทำให้การแก้จุดบกพร่องค่อนข้างยาก!
Matt Connolly

6
นี้ไม่เกิดขึ้นกับรุ่นแก้ปัญหานำมาจากคราสตราบใดที่คุณเริ่มต้นมันยังผ่าน Eclipse (หรือ IntelliJ หรือ IDE อื่น ๆ ) ไม่มีส่วนเกี่ยวข้องกับวิธีการติดตั้งแอปบนอุปกรณ์ ปัญหาคือเนื่องจากทาง app จะเริ่มต้น
David Wasser

2
ไม่มีใครรู้ว่ารหัสนี้จะทำให้แน่ใจว่าอินสแตนซ์ที่มีอยู่ของแอพจะถูกนำมาที่เบื้องหน้าหรือไม่? หรือเรียกว่าเสร็จสิ้น (); และปล่อยให้ผู้ใช้ไม่เห็นภาพว่ามีอะไรเกิดขึ้น?
Carlos P

5
@CarlosP หากกิจกรรมที่กำลังสร้างไม่ใช่กิจกรรมรูทของงานจะต้องมี(ตามนิยาม) มีกิจกรรมอื่นอย่างน้อยหนึ่งกิจกรรมที่อยู่ข้างใต้ หากกิจกรรมนี้เรียกfinish()ผู้ใช้จะเห็นกิจกรรมที่อยู่ด้านล่าง ด้วยเหตุนี้คุณจึงสามารถสันนิษฐานได้อย่างปลอดภัยว่าอินสแตนซ์ที่มีอยู่ของแอพจะถูกนำมาที่เบื้องหน้า หากไม่เป็นเช่นนั้นคุณจะมีหลายอินสแตนซ์ของแอปในงานแยกกันและกิจกรรมที่สร้างขึ้นจะเป็นรากฐานของงาน
David Wasser

27

ฉันจะอธิบายว่าทำไมมันถึงล้มเหลวและวิธีสร้างข้อผิดพลาดนี้โดยใช้โปรแกรมเพื่อให้คุณสามารถรวมสิ่งนี้ไว้ในชุดทดสอบของคุณ:

  1. เมื่อคุณเปิดแอปผ่าน Eclipse หรือ Market App แอปจะเปิดขึ้นพร้อมแฟล็กเจตนา: FLAG_ACTIVITY_NEW_TASK

  2. เมื่อเรียกใช้ผ่านตัวเรียกใช้งาน (ที่บ้าน) จะใช้แฟล็ก: FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_BROUGHT_TO_FRONT | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED และใช้การดำเนินการ " MAIN " และหมวดหมู่ " LAUNCHER "

หากคุณต้องการทำซ้ำสิ่งนี้ในกรณีทดสอบให้ใช้ขั้นตอนเหล่านี้:

adb shell am start -f 0x10000000 -n com.testfairy.tests.regression.taskroot/.MainActivity 

จากนั้นทำทุกอย่างที่จำเป็นเพื่อทำกิจกรรมอื่น ๆ เพื่อจุดประสงค์ของฉันฉันแค่วางปุ่มที่เริ่มกิจกรรมอื่น จากนั้นกลับไปที่ตัวเรียกใช้งาน (ที่บ้าน) ด้วย:

adb shell am start -W -c android.intent.category.HOME -a android.intent.action.MAIN

และจำลองการเปิดตัวผ่านตัวเรียกใช้งานด้วยสิ่งนี้:

adb shell am start -a "android.intent.action.MAIN" -c "android.intent.category.LAUNCHER" -f 0x10600000 -n com.testfairy.tests.regression.taskroot/.MainActivity

หากคุณยังไม่ได้รวมวิธีแก้ไขปัญหา isTaskRoot () สิ่งนี้จะทำให้เกิดปัญหาขึ้น เราใช้สิ่งนี้ในการทดสอบอัตโนมัติเพื่อให้แน่ใจว่าข้อบกพร่องนี้จะไม่เกิดขึ้นอีก

หวังว่านี่จะช่วยได้!


8

คุณได้ลองใช้โหมดเปิดตัวsingleTopหรือยัง?

นี่คือคำอธิบายบางส่วนจากhttp://developer.android.com/guide/topics/manifest/activity-element.html :

... อาจมีการสร้างอินสแตนซ์ใหม่ของกิจกรรม "singleTop" เพื่อจัดการกับความตั้งใจใหม่ อย่างไรก็ตามหากงานเป้าหมายมีอินสแตนซ์ของกิจกรรมอยู่แล้วที่ด้านบนสุดของสแต็กอินสแตนซ์นั้นจะได้รับเจตนาใหม่ (ในการเรียก onNewIntent ()) ไม่ได้สร้างอินสแตนซ์ใหม่ ในสถานการณ์อื่น ๆ ตัวอย่างเช่นหากอินสแตนซ์ที่มีอยู่ของกิจกรรม "singleTop" อยู่ในงานเป้าหมาย แต่ไม่อยู่ที่ด้านบนสุดของสแต็กหรือถ้าอยู่ที่ด้านบนสุดของสแต็ก แต่ไม่อยู่ในงานเป้าหมาย - ก อินสแตนซ์ใหม่จะถูกสร้างและพุชบนสแต็ก


2
ฉันคิดอย่างนั้น แต่ถ้ากิจกรรมไม่ได้อยู่ด้านบนสุดของสแต็กล่ะ? ตัวอย่างเช่นดูเหมือนว่า singleTop จะป้องกัน AA แต่ไม่ใช่ ABA
bsberkeley

คุณสามารถบรรลุสิ่งที่คุณต้องการโดยใช้ singleTop และวิธีการจบภายในกิจกรรมได้หรือไม่?
Eric Levine

ฉันไม่รู้ว่ามันจะสำเร็จอย่างที่ฉันต้องการหรือเปล่า ตัวอย่าง: ถ้าฉันอยู่ในกิจกรรม C หลังจากเปิด A และ B กิจกรรมใหม่ A จะเริ่มขึ้นและฉันจะมีบางอย่างเช่น CA ฉันจะไม่?
bsberkeley

มันยากที่จะตอบคำถามนี้โดยไม่เข้าใจมากขึ้นว่ากิจกรรมเหล่านี้ทำอย่างไร คุณสามารถให้รายละเอียดเพิ่มเติมเกี่ยวกับการสมัครและกิจกรรมของคุณได้หรือไม่? ฉันสงสัยว่าปุ่มโฮมทำอะไรไม่ตรงกันและคุณต้องการให้มันทำงานอย่างไร ปุ่มโฮมไม่ได้ออกจากกิจกรรม แต่จะ "พื้นหลัง" เพื่อให้ผู้ใช้เปลี่ยนไปใช้อย่างอื่นได้ ปุ่มย้อนกลับคือสิ่งที่ออก / เสร็จสิ้นและกิจกรรม การทำลายกระบวนทัศน์ดังกล่าวอาจทำให้ผู้ใช้สับสน / หงุดหงิด
Eric Levine

ฉันได้เพิ่มคำตอบให้กับชุดข้อความนี้เพื่อให้คุณสามารถดูสำเนาของไฟล์ Manifest ได้
bsberkeley

4

บางทีอาจเป็นปัญหานี้ ? หรือรูปแบบอื่น ๆ ของจุดบกพร่องเดียวกัน?


โปรดดูcode.google.com/p/android/issues/detail?id=26658ซึ่งแสดงให้เห็นว่าเกิดจากสิ่งอื่นที่ไม่ใช่ Eclipse
Kristopher Johnson

1
ดังนั้นฉันควรคัดลอกและวางคำอธิบายปัญหาที่อาจค้าง? ส่วนไหน? ควรเก็บส่วนสำคัญไว้หากลิงก์เปลี่ยนไปและเป็นความรับผิดชอบของฉันที่ต้องอัปเดตคำตอบอยู่เสมอ คุณควรคิดว่าลิงก์จะไม่ถูกต้องหากปัญหาได้รับการแก้ไขแล้วเท่านั้น นี่ไม่ใช่ลิงก์ไปยังบล็อก แต่อย่างใด
DuneCat

2

ฉันคิดว่าคำตอบที่ยอมรับ ( Duane Homick ) มีกรณีที่ไม่สามารถจัดการได้:

คุณมีความพิเศษที่แตกต่างกัน (และผลที่ซ้ำกันของแอป):

  • เมื่อคุณเปิดแอปพลิเคชันจาก Market หรือโดยไอคอนหน้าจอหลัก (ซึ่งวางโดย Market โดยอัตโนมัติ)
  • เมื่อคุณเปิดแอปพลิเคชันโดยตัวเรียกใช้งานหรือไอคอนหน้าจอหลักที่สร้างขึ้นเอง

นี่คือวิธีแก้ปัญหา (SDK_INT> = 11 สำหรับการแจ้งเตือน) ซึ่งฉันเชื่อว่าจะจัดการกรณีเหล่านี้และการแจ้งเตือนแถบสถานะด้วย

ประจักษ์ :

    <activity
        android:name="com.acme.activity.LauncherActivity"
        android:noHistory="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>
    <service android:name="com.acme.service.LauncherIntentService" />

กิจกรรมเปิด :

public static Integer lastLaunchTag = null;
@Override
public void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mInflater = LayoutInflater.from(this);
    View mainView = null;
    mainView = mInflater.inflate(R.layout.act_launcher, null); // empty layout
    setContentView(mainView);

    if (getIntent() == null || getIntent().getExtras() == null || !getIntent().getExtras().containsKey(Consts.EXTRA_ACTIVITY_LAUNCH_FIX)) {
        Intent serviceIntent = new Intent(this, LauncherIntentService.class);
        if (getIntent() != null && getIntent().getExtras() != null) {
            serviceIntent.putExtras(getIntent().getExtras());
        }
        lastLaunchTag = (int) (Math.random()*100000);
        serviceIntent.putExtra(Consts.EXTRA_ACTIVITY_LAUNCH_TAG, Integer.valueOf(lastLaunchTag));
        startService(serviceIntent);

        finish();
        return;
    }

    Intent intent = new Intent(this, SigninActivity.class);
    if (getIntent() != null && getIntent().getExtras() != null) {
        intent.putExtras(getIntent().getExtras());
    }
    startActivity(intent);
}

บริการ :

@Override
protected void onHandleIntent(final Intent intent) {
    Bundle extras = intent.getExtras();
    Integer lastLaunchTag = extras.getInt(Consts.EXTRA_ACTIVITY_LAUNCH_TAG);

    try {
        Long timeStart = new Date().getTime(); 
        while (new Date().getTime() - timeStart < 100) {
            Thread.currentThread().sleep(25);
            if (!lastLaunchTag.equals(LauncherActivity.lastLaunchTag)) {
                break;
            }
        }
        Thread.currentThread().sleep(25);
        launch(intent);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

private void launch(Intent intent) {
    Intent launchIintent = new Intent(LauncherIntentService.this, LauncherActivity.class);
    launchIintent.addCategory(Intent.CATEGORY_LAUNCHER);
    launchIintent.setAction(Intent.ACTION_MAIN); 
    launchIintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    launchIintent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 
    if (intent != null && intent.getExtras() != null) {
        launchIintent.putExtras(intent.getExtras());
    }
    launchIintent.putExtra(Consts.EXTRA_ACTIVITY_LAUNCH_FIX, true);
    startActivity(launchIintent);
}

การแจ้งเตือน :

ComponentName actCN = new ComponentName(context.getPackageName(), LauncherActivity.class.getName()); 
Intent contentIntent = new Intent(context, LauncherActivity.class);
contentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);    
if (Build.VERSION.SDK_INT >= 11) { 
    contentIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); // if you need to recreate activity stack
}
contentIntent.addCategory(Intent.CATEGORY_LAUNCHER);
contentIntent.setAction(Intent.ACTION_MAIN);
contentIntent.putExtra(Consts.EXTRA_CUSTOM_DATA, true);

2

ฉันตระหนักดีว่าคำถามไม่มีส่วนเกี่ยวข้องกับ Xamarin Android แต่ฉันต้องการโพสต์บางอย่างเนื่องจากฉันไม่เห็นที่อื่น

ในการแก้ไขปัญหานี้ใน Xamarin Android ฉันใช้รหัสจาก @DuaneHomick และเพิ่มเข้าไปในMainActivity.OnCreate()ไฟล์. แตกต่างกับ Xamarin คือว่าคือต้องไปหลังและXamarin.Forms.Forms.Init(this, bundle); LoadApplication(new App());ดังนั้นฉันOnCreate()จะดูเหมือน:

protected override void OnCreate(Bundle bundle) {
    base.OnCreate(bundle);

    Xamarin.Forms.Forms.Init(this, bundle);
    LoadApplication(new App());

    if(!IsTaskRoot) {
        Intent intent = Intent;
        string action = intent.Action;
        if(intent.HasCategory(Intent.CategoryLauncher) && action != null && action.Equals(Intent.ActionMain, System.StringComparison.OrdinalIgnoreCase)) {
            System.Console.WriteLine("\nIn APP.Droid.MainActivity.OnCreate() - Finishing Activity and returning since a second MainActivity has been created.\n");
            Finish();
            return; //Not necessary if there is no code below
        }
    }
}

* แก้ไข: ตั้งแต่ Android 6.0 วิธีแก้ปัญหาข้างต้นไม่เพียงพอสำหรับบางสถานการณ์ ตอนนี้ฉันได้ตั้งค่าLaunchModeเป็นSingleTaskซึ่งดูเหมือนว่าจะทำให้สิ่งต่างๆทำงานได้อย่างถูกต้องอีกครั้ง ไม่แน่ใจว่าสิ่งนี้อาจมีผลกระทบต่อสิ่งอื่นอย่างไร


0

ฉันมีปัญหาเดียวกันและฉันแก้ไขโดยใช้วิธีแก้ไขปัญหาต่อไปนี้

ในกิจกรรมหลักของคุณให้เพิ่มรหัสนี้ที่ด้านบนของonCreateวิธีการ:

ActivityManager manager = (ActivityManager) this.getSystemService( ACTIVITY_SERVICE );
List<RunningTaskInfo> tasks =  manager.getRunningTasks(Integer.MAX_VALUE);

for (RunningTaskInfo taskInfo : tasks) {
    if(taskInfo.baseActivity.getClassName().equals(<your package name>.<your class name>) && (taskInfo.numActivities > 1)){
        finish();
    }
}

อย่าลืมเพิ่มสิทธิ์นี้ในรายการของคุณ

< uses-permission android:name="android.permission.GET_TASKS" />

หวังว่าจะช่วยคุณได้


0

ฉันมีปัญหานี้ด้วย

  1. อย่าเรียกเสร็จ (); กิจกรรมในบ้านจะดำเนินไปอย่างไม่มีที่สิ้นสุด - ActivityManager จะเรียกกิจกรรมในบ้านเมื่อเสร็จสิ้น
  2. โดยปกติเมื่อการกำหนดค่ามีการเปลี่ยนแปลง (เช่นหมุนหน้าจอเปลี่ยนภาษาเปลี่ยนบริการโทรศัพท์เช่น mcc mnc เป็นต้น) กิจกรรมจะสร้างขึ้นใหม่ - และหากกิจกรรมที่บ้านกำลังทำงานอยู่จะเรียกอีกครั้งไปที่ A. สำหรับสิ่งนั้นที่จำเป็นต้องเพิ่มในรายการ android:configChanges="mcc|mnc"- ถ้า คุณมีการเชื่อมต่อกับเซลลูลาร์โปรดดูที่http://developer.android.com/guide/topics/manifest/activity-element.html#configว่ามีการกำหนดค่าใดบ้างเมื่อบูตระบบหรือกดเปิดหรืออะไรก็ตาม

0

ลองใช้วิธีนี้:
สร้างApplicationคลาสและกำหนดที่นั่น:

public static boolean IS_APP_RUNNING = false;

จากนั้นในกิจกรรม (ตัวเรียกใช้งาน) แรกของคุณonCreateก่อนsetContentView(...)เพิ่มสิ่งนี้:

if (Controller.IS_APP_RUNNING == false)
{
  Controller.IS_APP_RUNNING = true;
  setContentView(...)
  //Your onCreate code...
}
else
  finish();

ปล. ControllerคือApplicationชั้น.


คุณควรใช้บูลีนแบบดั้งเดิมซึ่งทำให้การตรวจสอบค่า null iun ไม่จำเป็น
WonderCsabo

สิ่งนี้จะไม่ได้ผลเสมอไป คุณจะไม่สามารถเปิดแอปของคุณออกจากแอปของคุณแล้วเปิดแอปของคุณอีกครั้งอย่างรวดเร็ว Android ไม่จำเป็นต้องฆ่ากระบวนการโฮสต์ OS ทันทีที่ไม่มีกิจกรรมที่ใช้งานอยู่ ในกรณีนี้เมื่อคุณเริ่มแอปอีกครั้งตัวแปรIS_APP_RUNNINGจะเป็นtrueและแอปของคุณจะปิดทันที ไม่ใช่สิ่งที่ผู้ใช้น่าจะรู้สึกขบขัน
David Wasser

-2

ลองใช้โหมดการเปิดใช้งาน SingleInstance ที่มีการตั้งค่าความสัมพันธ์เป็นallowtaskreparenting สิ่งนี้จะสร้างกิจกรรมในงานใหม่เสมอ แต่ยังอนุญาตให้ทำการ reparenting ตรวจสอบ: แอตทริบิวต์ Affinity


2
อาจใช้ไม่ได้เพราะตามเอกสาร "การเลี้ยงดูซ้ำถูก จำกัด ไว้ที่โหมด" มาตรฐาน "และ" singleTop "" เพราะ "กิจกรรมที่มีโหมดเปิดตัว" singleTask "หรือ" singleInstance "สามารถอยู่ที่ต้นตอของงานเท่านั้น"
bsberkeley

-2

ฉันพบวิธีที่จะป้องกันไม่ให้เริ่มกิจกรรมเดียวกันวิธีนี้ได้ผลดีสำหรับฉัน

if ( !this.getClass().getSimpleName().equals("YourActivityClassName")) {
    start your activity
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.