【问题标题】:Setting variable through protected function - lost reference通过受保护的功能设置变量 - 丢失参考
【发布时间】:2015-08-22 09:57:38
【问题描述】:

2017 年更新(正在进行中...)

总的来说,现在的情况是这样的。 我单击B 的实例(android 片段)并期望调用该实例的onContextItemSelected。确实调用了onContextItemSelected,但事实证明,这个类实例的方法是C

我被要求展示这个项目。在大约 2 年后查看此代码后,我决定我应该首先对其进行更多澄清,因为它没有超过 5% 的事件评论。我认为我今天不能做更多的事情了,我已经对整个项目进行了一些概述,所以你知道在哪里看。它还没有完全完成,可能包含一些错误,但更多的是它的样子:

明天我将尝试发布有关如何重现它的明确步骤。但是如果你想看的话,我已经包含了文件here

ABaseCustomFragment

BC 是扩展它的类。

我有这样的东西:

public class A extends Fragment implements OnItemClickListener, OnItemLongClickListener{
    protected D loc;

    protected void setContext(D l){
        Log.d("A", "setContext :" + String.valueOf(l));
        loc = l;
        Log.d("A", "setContext2 :" + String.valueOf(loc));
    }

    public boolean onContextItemSelected(MenuItem item) {
        Log.d("A", "itemSelected :" + String.valueOf(loc));
    }
}

public class B extends A{

    public boolean onItemLongClick(AdapterView<?> pr, View view,int p, long id) {
        D d = (D) pr.getItemAtPosition(p);
        Log.d("B", "longClick :" + String.valueOf(d));
        setContext(d);
        return false;
    }
}

日志如下所示:

B 长按:数据

一个 setContext :data

一个 setContext2 :data

A itemSelected :null

我不会在其他任何地方碰locsetContex。我完全不知道发生了什么。怎么可能?

我将A 类设置为ListView 监听器。我在ViewPager 中使用这个片段。 onItemSelectedsetContext 之后被调用。 不知道该说些什么。

编辑:

declate as volatile 并没有修复它,但是...... 更奇怪的东西 - 在课堂上 A 我有:

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    lv = (ListView) inflater.inflate(layoutResourceId, container, false);
    lv.setOnItemClickListener(this);
    lv.setOnItemLongClickListener(this);
    lv.setAdapter(adapter);
    //this.registerForContextMenu(lv);
    return lv;
}

我现在也加入了B 类:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    super.onCreateView(inflater, container, savedInstanceState);
    this.registerForContextMenu(lv);
    return lv;
}
@Override
public boolean onContextItemSelected(MenuItem item) {
    //Never called!
    Log.d("Loc", "selected :" + name);
    return super.onContextItemSelected(item);
}

神奇之处在于我还有 C 类,它与 B 基本相同,只是我没有覆盖上面的那些额外方法:

public class C extends A {      
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        ...
    }   
    @Override
    public boolean onItemLongClick(AdapterView<?> parent, View view,int position, long id) {
        ...
    }       
}

日志显示:

B longClick :data - 来自 B 的实例

一个 setContext :data - 来自 B 的实例

一个 setContext2 :data - 来自 B 的实例

A itemSelected :null - 来自 C 的实例!!!

那怎么样?我只在课堂上打电话给registerForContext B 有什么想法吗?

【问题讨论】:

  • 我打电话给registerForContextMenu 上课A 传递ListView。我应该把它包括在内吗?
  • 你能发布一个基本的项目吗?
  • 您可能会发现以下部分或全部内容很有帮助。 1)您可以拥有多个类的实例。验证 loc 是否被清除,或者仍然是不同实例中的初始空值。 2)使用调试器查看验证您正在使用哪些实例,以及何时/是否清除 loc 。 3) 避免使用受保护的变量,以确保没有其他类可以修改 loc。将 loc 设为私有。
  • @azizbekian 好吧,如果你愿意,我可以发布整个项目 - 它有点被放弃了。但是要分析的文件很多,我不知道如何更好地剥离它。
  • @AndyThomas 查看最后的引用 - 看起来我调用了B 实例的方法,但不知何故,itemSelected 是从另一个类实例调用的,甚至是不同的类CBC 都扩展了 A

标签: java android variables


【解决方案1】:

如果您的A 对象正在从多个线程访问(这似乎很可能给您的事件侦听器),您会看到这样的问题(它的通用术语是"stale data")。要快速检查这是否是线程问题,请将您的成员变量声明为protected volatile D loc; 并查看问题是否消失(请参阅this resource about volatile)。如果这确实解决了问题,您将需要实施更高级的线程保护,以确保您不会遇到任何更邪恶/微妙的线程错误。

【讨论】:

    【解决方案2】:

    更新

    出了什么问题?

    您将ViewPagerFragmentPagerAdapter 结合使用。这种组合的一个重要实现细节是您有几个Fragments 附加到Activity。这是ViewPager 工作所必需的,因为它至少需要下一个和上一个Fragments 及其Views 才能准备好交换。

    问题在于,当您仅使用标题发送MenuItems 时,您没有考虑ViewPager 的这种行为。这通常是一个可怕的想法,这只是为什么它是错误的示例。当显示上下文菜单并且用户选择某个项目时,首先MainActivity 有机会处理它,然后事件转到FragmentManager,后者将事件分派给all 附上Fragments(因为你想想也没有其他合理的选择)。因此,您的每个Fragments 都会收到onMenuItemSelected 调用并尝试处理它,因为MenuItem 标题是相同的。当然你有一个例外,因为对于其他Fragments 当前选定项目的“上下文”没有设置。

    如何解决?

    显然不要只使用标题来为您的MenuItems 发送事件。您实际上可以为每个Fragment 生成唯一的groupIditemId。但我更喜欢类似 OOP 的方式。首先摆脱所有原始的上下文菜单管理代码(menusmpossetMenuaddMenuItemremoveItemMenuonMenuItemSelected 等)并用类似的代码替换它们

    public class BaseCustomFragment extends Fragment implements OnItemClickListener, OnItemLongClickListener
    {
    
        private List<MenuItemAction> menus = new ArrayList<MenuItemAction>();
    
        // base class for all actions for context-menu in BaseCustomFragment 
        abstract class MenuItemAction implements MenuItem.OnMenuItemClickListener
        {
            private final String title;
    
            public MenuItemAction(String title)
            {
                this.title = title;
            }
    
            public String getTitle()
            {
                return title;
            }
    
            public final boolean isVisible()
            {
                return isVisibleImpl(conCon, locCon);
            }
    
            @Override
            public final boolean onMenuItemClick(MenuItem item)
            {
                AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
                Log.d("MenuItem", "menu item '" + title + "' for #" + info.position + " of  " + BaseCustomFragment.this.getClass().getSimpleName());
                handleActionImpl(conCon, locCon);
                return true;
            }
    
            protected final void startActivity(Uri uri)
            {
                startActivity(Intent.ACTION_VIEW, uri);
            }
    
            protected final void startActivity(String action, Uri uri)
            {
                startActivity(new Intent(action, uri));
            }
    
            protected final void startActivity(Class<? extends Activity> activityClass)
            {
                startActivity(new Intent(getActivity(), activityClass));
            }
    
            protected final void startActivity(Intent intent)
            {
                getActivity().startActivity(intent);
            }
    
            protected abstract boolean isVisibleImpl(Customer conCon, Localization locCon);
    
            protected abstract void handleActionImpl(Customer conCon, Localization locCon);
    
        }
    
        protected void setContext(Customer c, Localization l)
        {
            Log.d("BaseFrag", "setContext class:" + String.valueOf(this.getClass()));
            Log.d("BaseFrag", "setContext :" + String.valueOf(l));
            conCon = c;
            locCon = l;
            Log.d("BaseFrag", "setContext2 :" + String.valueOf(locCon));
            updateContextMenu();
        }
    
        protected void setMenus(MenuItemAction... menuItems)
        {
            this.menus = Arrays.asList(menuItems);
            updateContextMenu();
        }
    
        @Override
        public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info)
        {
            super.onCreateContextMenu(menu, v, info);
    
            for (MenuItemAction menuItemAction : menus)
            {
                if (menuItemAction.isVisible())
                {
                    MenuItem menuItem = menu.add(menuItemAction.getTitle());
                    menuItem.setOnMenuItemClickListener(menuItemAction);
                }
            }
        }
    
    
        private void updateContextMenu()
        {
            if (lv == null)
                return;
            boolean hasVisibleItems = false;
            for (MenuItemAction menuItemAction : menus)
            {
                if (menuItemAction.isVisible())
                {
                    hasVisibleItems = true;
                    break;
                }
            }
            if (hasVisibleItems)
            {
                Log.d(getClass().getSimpleName(), "Attaching context menu for " + getClass().getSimpleName());
                this.registerForContextMenu(lv);
            }
            else
            {
                Log.d(getClass().getSimpleName(), "Detaching context menu for " + getClass().getSimpleName());
                this.unregisterForContextMenu(lv);
            }
        }
    

    这里MenuItemAction 是每个Fragment 独立持有的所有“内部”菜单项的基类。我称它们为“内部”,因为“真实”MenuItems 是实现细节,您不能从它们继承,因此我们将从“内部”的创建“真实”。此转换/创建在 onCreateContextMenu 内部完成。请注意,MenuItemAction 将已经提供“上下文”数据的事件分派到需要的地方。它还包含一些startActivity 帮助方法来简化代码。

    现在我们可以向BaseCustomFragment 添加一堆帮助方法来创建您的典型菜单项:

        protected MenuItemAction createNavigateToLocationMenuItem()
        {
            return new MenuItemAction("Navigate to location")
            {
                @Override
                protected boolean isVisibleImpl(Customer conCon, Localization locCon)
                {
                    return (locCon != null);
                }
    
                @Override
                protected void handleActionImpl(Customer conCon, Localization locCon)
                {
                    startActivity(Uri.parse("google.navigation:q=" + Uri.encode(locCon.noZipString())));
                }
            };
        }
    
        protected MenuItemAction createCallMenuItem()
        {
            return new MenuItemAction("Call")
            {
                @Override
                protected boolean isVisibleImpl(Customer conCon, Localization locCon)
                {
                    return (conCon != null) && (conCon.getPhoneNumbers().size() > 0);
                }
    
                @Override
                protected void handleActionImpl(Customer conCon, Localization locCon)
                {
                    startActivity(Intent.ACTION_DIAL, Uri.parse("tel:" + Uri.encode(BaseCustomFragment.this.conCon.getPhoneNumbers().get(0).toString())));
                }
            };
        }
    
        protected MenuItemAction createSmsMenuItem()
        {
            return new MenuItemAction("SMS")
            {
                @Override
                protected boolean isVisibleImpl(Customer conCon, Localization locCon)
                {
                    return (conCon != null) && (conCon.getPhoneNumbers().size() > 0);
                }
    
                @Override
                protected void handleActionImpl(Customer conCon, Localization locCon)
                {
                    startActivity(Uri.parse("sms:" + Uri.encode(BaseCustomFragment.this.conCon.getPhoneNumbers().get(0).toString())));
                }
            };
        }
    
        protected MenuItemAction createEmailMenuItem()
        {
            return new MenuItemAction("Email")
            {
                @Override
                protected boolean isVisibleImpl(Customer conCon, Localization locCon)
                {
                    return (conCon != null) && (conCon.getEmail().length() > 0);
                }
    
                @Override
                protected void handleActionImpl(Customer conCon, Localization locCon)
                {
                    startActivity(Uri.parse("mailto:" + Uri.encode(conCon.getEmail())));
                }
            };
        }
    
        protected MenuItemAction createNewOrderMenuItem()
        {
            return new MenuItemAction("New Order")
            {
                @Override
                protected boolean isVisibleImpl(Customer conCon, Localization locCon)
                {
                    return true;
                }
    
                @Override
                protected void handleActionImpl(Customer conCon, Localization locCon)
                {
                    startActivity(OrderActivity.class);
                }
            };
        }
    

    所以剩下的就是使用这个基础设施特定的片段,例如

    public class CustomersFrag extends BaseCustomFragment
    {
        public CustomersFrag()
        {
    
            ... 
    
            setMenus(createCallMenuItem(),
                     createSmsMenuItem(),
                     createEmailMenuItem(),
                     createNewOrderMenuItem(),
                     createNavigateToLocationMenuItem());
        }
    
        ... 
    
        @Override
        public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id)
        {
            Customer c = (Customer) parent.getItemAtPosition(position);
            setContext(c, c.getLocalization());
            return super.onItemLongClick(parent, view, position, id);
        }
    

    public class LocalizationsFrag extends BaseCustomFragment
    {
        public LocalizationsFrag()
        {
    
            ... 
    
            setMenus(
                    createNavigateToLocationMenuItem(),
                    createNewOrderMenuItem());
        }
    
        ... 
    
        @Override
        public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id)
        {
            Localization o = (Localization) parent.getItemAtPosition(position);
            setContext(null, o);
            return super.onItemLongClick(parent, view, position, id);
        }
    

    请注意,您的代码中的菜单项标题不再有 if/elseswitch。此外,您不必根据特定片段中的数据在每个onItemLongClick 中执行奇怪的addMenuItem/removeMenuItem,因为:

    1. 现在每个MenuItemAction 都有自己的isVisibleImplhandleActionImpl 方法
    2. setContext 致电updateContextMenu

    All for you menu 操作似乎是通用的,并且适合您已有的简单“上下文”。因此,我创建了MenuItemAction 只是一个非静态内部类,可以使用BaseCustomFragment.this 获取它的父级Fragment。如果在某些时候这成为一个限制,并且您想创建一个特定于 BaseCustomFragment 的子子类的操作,并且您希望它使用来自该片段的数据,您只需在相应的子类中创建菜单项。想象一下,您想添加一个“复制订单”菜单项。你可以这样做:

    public class OrdersFragment extends BaseCustomFragment
    {
    
        ... 
    
    
        private Order currentOrder;
    
        public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id)
        {
            Order o = (Order) parent.getItemAtPosition(position);
            currentOrder = o;
            setContext(o.getCustomer(), o.getLocalization());
    
            return super.onItemLongClick(parent, view, position, id);
        }
    
        protected MenuItemAction createNewOrderMenuItem()
        {
            return new MenuItemAction("Copy Order")
            {
                @Override
                protected boolean isVisibleImpl(Customer conCon, Localization locCon)
                {
                    return (OrdersFragment.this.currentOrder != null);
                }
    
                @Override
                protected void handleActionImpl(Customer conCon, Localization locCon)
                {
                    Intent intent = new Intent(getActivity(), OrderActivity.class);
                    intent.putExtra("copySrcId", OrdersFragment.this.currentOrder.getId());
                    startActivity(intent);
                }
            };
        }
    

    现在您的“复制订单”MenuItemAction 可以访问在OrdersFragment 中定义的currentOrder

    旁注

    查看您的代码,我发现还有一些我认为值得一提的事情:

    • 对电话号码使用整数类型是个坏主意。电话号码可能包含其他符号,例如“+”或“*”或“#”,它们不适合基于整数的类型。电话号码可能以“0”开头,您将无法转换为基于整数的类型。
    • 我认为你应该更好地学习java.util包。例如,java.util.List 及其子类中有一个 contains 方法。
    • 当您可以使用超类/接口时,为什么要在声明中使用非常具体的类型尚不清楚。例如,您的所有模型类似乎都使用java.sql.Timestamp 作为日期字段,没有明显的原因(您真的需要纳秒吗?)。或者您在任何地方都使用ArrayList,尽管有时只需List 就足够了
    • prepareTabs 中的MainActivity 中,添加标签后不要调用adapter.notifyDataSetChanged();。实际上,这会导致更新的支持库崩溃。

    希望对你有帮助


    旧答案(很可能不相关)

    this 添加到每个Log 呼叫。我怀疑答案是在

    B 长按:数据

    一个 setContext :data

    一个 setContext2 :data

    A itemSelected :null

    不知何故,最后一个 Log 是针对前 3 个不同的对象完成的。同样,问题不是每次都可重现,这是真的吗?如果是这样,它可能与设备旋转或在事件处理过程中触发 Activity 重新创建的其他因素有关。因此,您可以将日志添加到您的 onPauseonResume 并查看是否是这种情况。

    【讨论】:

    • 你添加了什么“将this 添加到每个Log 呼叫”?我已经在日志调用中追踪到String.valueOf(this.getClass()),结果显示在最后一个引号中——最后一个调用来自不同类的实例,所以它显然不是同一个实例。 :)
    • @PawełAudionysos,我建议登录 this 而不是 this.getClass(),这样你就可以看到像 you.package.A@1234#hashCode 这样的东西,这对于不同的对象来说会更独特,但是是的,我错了,你更新的日志已经说过最后一行来自不同的对象。所以我的下一个想法是在你的班级A 中覆盖registerForContextMenu 并在那里登录this,可能使用Thread.currentThread().getStackTrace() 进行堆栈跟踪,这样你就可以了解C 是如何在那里注册的。附言如果问题很容易重现,您没有回答。
    • @PawełAudionysos,如果错误是可重现的,并且您愿意分享代码并提供清晰的重现步骤,那么有人愿意调试(很可能包括我0
    • 我相信这个错误是持久的,但老实说我从那时起就没有编译它,所以我需要刷新我的记忆。我今天会发布一些更新,试图让问题更清楚,也许我会发布代码。你能告诉我我应该如何发布这个项目吗?它有很多文件,我正在根据数据库中的数据生成这些列表。代码非常通用。对我来说最简单的方法是发布整个项目,但我不想因为它的复杂性吓到任何人。我也应该将它发布在 github 上还是打包到存档中?你怎么看?
    • @PawełAudionysos,我使用了您的代码,但无法重现该问题。我用一些假数据预先填写了所有标签。我假设BLocalizationsFrag,你必须在那里使用上下文菜单做一些事情,但乍一看似乎工作正常。因此,欢迎采取重现步骤。还是我也必须更改代码中的某些内容?
    猜你喜欢
    • 1970-01-01
    • 2011-08-02
    • 1970-01-01
    • 2020-06-26
    • 1970-01-01
    • 1970-01-01
    • 2020-12-07
    • 1970-01-01
    • 2021-01-09
    相关资源
    最近更新 更多