Android Navigation 遇坑记 - 真实项目经历

发表于 2年以前  | 总阅读数:2602 次

Android Navigation 是 Google Jetpack 里面的一个组件,支持 Android 应用里面的页面导航。

我们开发的应用在 2.0 大版本的迭代的时候选择了这个技术方案,现在版本刚提测,趁着新鲜出炉,把遇到的坑和大家分享一下。

先声明一下,这篇文章不是 Navigation 的入门文章和使用说明,也不是原理介绍,可以作为我们团队对 Navigation 使用的一些心得体会的说明,比较适合对 Navigation 有项目实践的同学。

这里也欢迎有兴趣的同学,如果有这方面想交流的话,欢迎联系,一起探讨。以下都是一些从项目实践中的个人见解,免不了出纰漏,不吝斧正。

Navigation 优点体会

在使用过程中,我们感受到如下的优点。

  1. 页面跳转性能更好,在单 Activity 的架构下,都是 fragment 的切换,每次 fragment 被压栈之后,View 被销毁,相比之前 Activity 跳转,更加轻量,需要的内存更少。
  2. 通过 Viewmodel 进行数据共享更便捷,不需要页面之间来回传数据。
  3. 统一的 Navigation API 来更精细的控制跳转逻辑。

所有坑的中心

Navigation 相关的坑,都有个中心。一般情况下,Fragment 就是一个 View,View 的生命周期就是 Fragment 的生命周期,但是在 Navigation 的架构下,Fragment 的生命周期和 View 的生命周期是不一样的。当 navigate 到新的 UI,被覆盖的 UI,View 被销毁,但是保留了 fragment 实例(未被 destroy),当这个 fragment 被 resume 的时候,View 会被重新创建。这是“罪恶”之源。

整理了 8 个坑,我们一个一个趟

先来个小坑感受一些 Navigation。

1. Databinding 需要 onDestroyView 设置为 Null。

现在大家都会使用 Jetpack 里面的 databinding 技术,这个确实可以帮助我们简化很多代码,其中的自感知的生命周期,可以帮我们只有在必要的时候来更新 UI。

一般会在 Fragment 的 onCreateView 模板函数中初始化 ViewDataBing,这样就会有 Fragment 持有对 View 的引用。但是 fragment 和 view 的生命周期是不一样的,当 view 被销毁的时候,fragment 并不一定被销毁,所以一定要在 fragment.onDestroyView 函数中把对 view 的引用变量设置为 null,不然会导致 view 回收不掉。上一段官方的代码来说明一下。

private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    _binding = ResultProfileBinding.inflate(inflater, container, false)
    val view = binding.root
    return view
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}

2. 当 Databinding 遇到错的 lifecycle.

Databinding 确实很强大,能把数据和 UI 进行绑定,这里对 UI 就有个要求,UI 一定要知道自己的生命周期的,知道自己什么时候处于 Active 和 InActive 的状态。所以我们必须要给 databinding 设置一个正确的生命周期.

下面来看一段有问题的代码:

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    _binding = HomeFragmentBinding.inflate(inflater, container, false)
    binding.lifecycleOwner = this // 问题代码在这里!!!
    return binding.root
}

这段代码运行起来没有问题,看起来都是按照预期的在执行。甚至官方代码也是这么写的。连 LeakCanary 也检测不出来内存泄漏的问题,LeakCanary 只能检测出来一些 Activity,Fragment 和 View 等实例的内存泄漏,对于普通的类的实例是没有办法分析的。

问题就出现在 databinding 遇到了一个错的 lifecycle,在没有用 Navigation 框架的时候,View 的生命周期和 Fragment 的生命周期一致的,但是在 Navigation 框架下,两者的生命周期是不一致的。我们来看下 ViewDataBinding 设置 lifecycleOwner 的具体代码。

下面的代码中,往这个 lifecycleOwner 里面加入了一个 OnStartListener 实例,因为这个 lifecycleOwner 是 fragment 的,会在 fragment 销毁的时候反注册,但是并不会在 View 被销毁的时候被反注册。而 OnStartListener 有对这个 ViewDataBinding 有引用,会导致 View 被销毁的时候(跳到另外一个页面),这个引用会阻止系统回收这个 View。

这个分析逻辑是对的,但是结果是不对的,系统还是会对这个 View 进行回收,因为 OnStartListener 的实例持有的是对这个 View 的弱引用,这个 View 还是会被回收。这就是 LeakCanary 没有报错的原因。但是这个 OnStartListener 的实例,就没这么幸运了,正是这个实例无法回收导致了内存泄漏。

@MainThread
public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) {
    if (mLifecycleOwner == lifecycleOwner) {
        return;
    }
    if (mLifecycleOwner != null) {
        mLifecycleOwner.getLifecycle().removeObserver(mOnStartListener);
    }
    mLifecycleOwner = lifecycleOwner;
    if (lifecycleOwner != null) {
        if (mOnStartListener == null) {
            mOnStartListener = new OnStartListener(this);
            // 这个实例持有了ViewDataBinging的实例,虽然是弱引用。
        }
        lifecycleOwner.getLifecycle().addObserver(mOnStartListener);
        // 问题出现在这里,如果这个lifecycle是fragment的,View被销毁了,里面不会进行反注册。
    }
    for (WeakListener<?> weakListener : mLocalFieldObservers) {
        if (weakListener != null) {
            weakListener.setLifecycleOwner(lifecycleOwner);
        }
    }
}

正确的做法是需要给这个 ViewDataBinding 设置 viewLifecycleOwner.

binding.lifecycleOwner = viewLifecycleOwner

多说一句啊,这个问题是如何被发现的呢?我们有一套检查框架逻辑的代码,对于这个问题,我们会在 fragment 的 onStop 函数里面检查有多少实例在监听 fragment 的生命周期,我们发现,这个数字会一直涨,这个问题就暴露了,适当的时候,和大家分享一下这套框架。

3. Glide 自我管理的生命周期值得信赖吗?

不值得信赖了

Glide 是一个非常流行的图片加载框架,不得不说,Glide 的缓存这一块的设计非常的优秀,功能强大,可扩展强。还有它的生命周期的自我管理,通过创建一个 fragment 在当前的页面,通过这个 fragment 的生命周期,实现在 onStart 的时候进行图片加载,在 onStop 的时候,把还没有执行或者没有执行完的任务缓存下来,以便在 onStart 的再执行,当然是在没有 onDestory 的情况下。

一切都很完美,直到遇到了 Navigation。

glide.with(fragment).load(url).into(imageview).

呵呵,上面的这段,在 Navigation 的架构下,如果 Fragment 还在,但是执行了 onDestroyView,imageview 需要被销毁。这个情况下,如果图片加载任务没执行完,任务就会被缓存下来了。这个任务还有对需要被销毁的 imageview 有强引用,导致这个 imageview 销毁不了,从而内存泄漏。

如何 100%的重现这个问题呢,有个简单的方法,让大家可以验证一下这个问题。给这个任务,加一个图片的 transformation,这个 transformation 什么也不干,就是 sleep 3 秒钟,在这个 3 秒中之内,跳转到另一个页面。这会导致当前页面进行 View 的 destory,但是 fragment 并不会 destory,因为这个任务还没执行完,这个任务就会被 Glide 缓存,具体会被缓存位置为 RequestManager->RequestTracker->pendingRequests。

如何来解决这个问题呢?这个没有现成的解决方法,在 Glide 的官网有提类似的问题,但是 Glide 维护者听起来还没有意识到这个问题,没有后续的计划。当然,我们需要来解决这个问题,不然我们的代码就会存在这一点瑕疵了。

解决的方法:自己来管理 Glide 的生命周期,不要通过那个看不见的 fragment 的生命周期,因为那是靠不住的。我们自己写了一个 RequestManager,通过传入的 fragment 的 viewLifecycleOwner 来进行管理。使用也很方便,在调用的时候如下即可。

KGlide.with(fragment).load(url).into(imageview).

源码精简了一下,贴在这里,请指正。

import com.bumptech.glide.manager.Lifecycle as GlideLifecycle

class KGlide {

    companion object {
        private val lifecycleMap = ArrayMap<LifecycleOwner, RequestManager>()

        @MainThread
        fun with(fragment: Fragment): RequestManager {
            Util.assertMainThread()

            val lifecycleOwner = fragment.viewLifecycleOwner
            if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
                throw IllegalStateException("View is already destroyed.")
            }

            if (lifecycleMap[lifecycleOwner] == null) {
                val appContext = fragment.requireContext().applicationContext
                lifecycleMap[lifecycleOwner] = RequestManager(
                    Glide.get(appContext),
                    KLifecycle(lifecycleOwner.lifecycle),
                    KEmptyRequestManagerTreeNode(), appContext
                )
            }
            return lifecycleMap[lifecycleOwner]!!
        }
    }

    class KEmptyRequestManagerTreeNode : RequestManagerTreeNode {
        override fun getDescendants(): Set<RequestManager> {
            return emptySet()
        }
    }

    class KLifecycle(private val lifecycle: Lifecycle) : GlideLifecycle {
        private val lifecycleListeners =
            Collections.newSetFromMap(WeakHashMap<LifecycleListener, Boolean>())

        private val lifecycleObserver = object : DefaultLifecycleObserver {
            override fun onStart(owner: LifecycleOwner) {
                val listeners = Util.getSnapshot(lifecycleListeners)
                for (listener in listeners) {
                    listener.onStart()
                }
            }

            override fun onStop(owner: LifecycleOwner) {
                val listeners = Util.getSnapshot(lifecycleListeners)
                for (listener in listeners) {
                    listener.onStop()
                }
            }

            override fun onDestroy(owner: LifecycleOwner) {
                val listeners = Util.getSnapshot(lifecycleListeners)
                for (listener in listeners) {
                    listener.onDestroy()
                }

                lifecycleMap.remove(owner)
                lifecycleListeners.clear()
                lifecycle.removeObserver(this)
            }
        }

        init {
            lifecycle.addObserver(lifecycleObserver)
        }

        override fun addListener(listener: LifecycleListener) {
            lifecycleListeners.add(listener)
            when (lifecycle.currentState) {
                Lifecycle.State.STARTED, Lifecycle.State.RESUMED -> listener.onStart()
                Lifecycle.State.DESTROYED -> listener.onDestroy()
                else -> listener.onStop()
            }
        }

        override fun removeListener(listener: LifecycleListener) {
            lifecycleListeners.remove(listener)
        }
    }
}

4. Android 组件的生命周期自我管理值得信任吗?

不值得,信任需要我们对 Android 生命周期的管理细节足够的了解。没有足够的了解,哪里来的信任,也就是盲目的信任。

我们在 Android 官方文档里面应该看到过 LiveData 的介绍,下面摘录一段。

Livedata is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.

然后还向我们说明 Livedata 的不会导致内存泄漏。

This is especially useful for activities and fragments because they can safely observe LiveData objects and not worry about leaks—activities and fragments are instantly unsubscribed when their lifecycles are destroyed.

写的很清楚,言之昭昭啊。如果你相信了官方文档的介绍,就 too young,too simple 了。LiveData 未必会在 lifecycleOwner 销毁的时候进行反注册,内存泄漏还是会发生。我们看一段 LiveData 会产生内存泄漏的代码。

class HomeFragment : Fragment() {
    private val model: NavigationViewModel by viewModels()

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.home_fragment, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        model.getTextValue().observe(viewLifecycleOwner){
            view.findViewById<Button>(R.id.text).text = it
        }

        if (isXXX()) {
            findNavController().navigate(R.id.next_action)
        }
    }
}

当你进入某个页面,发现需要导航到另一个页面,这个时候就需要很小心。如果像上面这样的写法,就会导致内存泄漏。

这个 Case 里,在 Fragment.onViewCreated()的模板方法,监听了一个 LiveData,这会导致这个 LiveData 持有外面对象的引用。理想情况下,这个 LivaData 会在 LifecycleOwner 在 onDestory 的时候进行反注册,但是在一些情况下,这个反注册就不会进行。

如上代码的情况下,如果这个页面马上跳到 next_action 的页面,之前订阅的 LiveData 就不会进行反注册。原因出在当跳出这个页面的时候,页面还处于生命周期的状态 INITIALIZED,但是反注册的条件是这个页面的生命周期状态至少是 CREATED.

void performDestroyView() {
    mChildFragmentManager.dispatchDestroyView();
    if (mView != null && mViewLifecycleOwner.getLifecycle().getCurrentState()
                    .isAtLeast(Lifecycle.State.CREATED)) {
        mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
    }
    ......

}

其实 Android 的生命周期管理还是值得信任的,前提是我们得彻底搞清楚状态流转的细节。

5. 当 ViewPager2 遇到 Navigation

ViewPager 是在应用开发的过程中,高频的用到的组件。Android 的官网有对基本的使用有详细的介绍。

一直都很美好,直到遇到 Navigation。

让我们来看官方例子里面 ViewPager2 的 Adapter 的类的声明。

class DemoCollectionAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {

    override fun getItemCount(): Int = 100

    override fun createFragment(position: Int): Fragment {
        // Return a NEW fragment instance in createFragment(int)
        val fragment = DemoObjectFragment()
        fragment.arguments = Bundle().apply {
            // Our object is just an integer :-P
            putInt(ARG_OBJECT, position + 1)
        }
        return fragment
    }
}

不避讳的说,我们实际项目中的代码,也犯了同样的问题。不是说官网的写法有问题,而是在 Navigation 的框架下,才会导致的内存泄漏问题。这个泄漏是如何发生的呢?我们来看一下 FragmentStateAdapter 的构造函数。

/**
 * @param fragment if the {@link ViewPager2} lives directly in a {@link Fragment} subclass.
 *
 * @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity)
 * @see FragmentStateAdapter#FragmentStateAdapter(FragmentManager, Lifecycle)
 */
public FragmentStateAdapter(@NonNull Fragment fragment) {
    this(fragment.getChildFragmentManager(), fragment.getLifecycle());
}
/**
 * @param fragmentManager of {@link ViewPager2}'s host
 * @param lifecycle of {@link ViewPager2}'s host
 *
 * @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity)
 * @see FragmentStateAdapter#FragmentStateAdapter(Fragment)
 */
public FragmentStateAdapter(@NonNull FragmentManager fragmentManager,
        @NonNull Lifecycle lifecycle) {
    mFragmentManager = fragmentManager;
    mLifecycle = lifecycle;
    super.setHasStableIds(true);
}

可以看到 FragmentStateAdapter 最终会走两个参数的构造函数。第一个是 fragmentManager of ViewPager2's Host,第二个参数是 lifecycle of ViewPager2's host。如果你看懂了之前的问题,你就会知道这个问题出在哪里了。在 Navigation 下面,fragment 和 view 的生命周期是不一致的,如果我们在 FragmentStateAdapter 的构造函数中,只传入 fragment 的实例的话,第二个参数 lifecycle 用的是第一个参数 fragment 的 lifecycle。但是很显然,viewpager2's host 的 lifecycleOwner 是 fragment 的 viewlifecycleOwner,而不是其本身。

具体导致的问题是,在 ViewPager2 实例被销毁的时候,对应的 FragmentStateAdapter 并不会被销毁,因为如果只传一个参数的话,使用的是 Fragment 的生命周期,只有在 fragment 退出的时候,才会被销毁。

这里多说一句啊,FragmentStateAdapter 实例不能被设置到多个 ViewPager2 的对象,所以当 ViewPager2 被重建的时候,这个 Adapter 不能被重用。

这些问题其实很难被发现,LeakCanary 也不能发现。幸好我们有个工具,在每个需要被检查的类的构造函数里面进行记录,然后在类的 finalize 方法对这个记录进行处理,如果发现某个类一直被构造,但是不执行 finalize 方法,这个类就需要被好好关照了。

6. ViewPager2 设置 Adapter 导致的 Fragment 重建问题

先来看以下的代码片段:

Line1:val viewPager2: ViewPager2 = ......
Line2:val adapter: FragmentStateAdapter = ......
Line3:viewPager2.adapter = adapter
Line4:model.getContentList.observe(viewLifecycleOwner) {
Line5:    adapter.data = it
Line6:    adapter.notifyDataSetChanged()
Line7:}

大家应该看不出来这段代码的问题所在的吧,这个是非常常规的写法。当然这段代码在非 Navigation 的架构下面是没有问题的。但是如果在 Navigation 的架构下,就会有比较严重的问题了。

说明一下问题出现的场景,如果用户先进入这个页面,执行上面代码,viewpager 正常显示。然后注意,重要的步骤来了,在这个页面上,导航到另外一个页面。那当前的这个页面会执行 fragment 的 onStop,注意并不会执行 onDestory。但是会执行 onDestoryView,也就是说 viewPager 将会被销毁,但是 fragment 被保留了。

那如果重新回到这个页面会发生什么事情呢,之前 onStop 的 fragment 会执行 onStart,包括 Adatper 里面生成的 fragment 也会进行重建,并创建 View。

出人意料的事情发生了,Adatper 里面的 fragment 在重建完成之后,立刻又被销毁掉了,这里的销毁是真正的销毁,执行了 onDestory 方法。然一个新的 fragment 被重新创建出来,这就是 fragment 重建问题。是什么导致了这个问题呢?

具体执行销毁 Fragment 的代码如下,在 FragmentStateAdapter 的 gcFragments 的方法。

void gcFragments() {
    if (!mHasStaleFragments || shouldDelayFragmentTransactions()) {
        return;
    }

    // Remove Fragments for items that are no longer part of the data-set
    Set<Long> toRemove = new ArraySet<>();
    for (int ix = 0; ix < mFragments.size(); ix++) {
        long itemId = mFragments.keyAt(ix);
        if (!containsItem(itemId)) {
            toRemove.add(itemId);
            mItemIdToViewHolder.remove(itemId); // in case they're still bound
        }
    }

    // Remove Fragments that are not bound anywhere -- pending a grace period
    if (!mIsInGracePeriod) {
        mHasStaleFragments = false; // we've executed all GC checks

        for (int ix = 0; ix < mFragments.size(); ix++) {
            long itemId = mFragments.keyAt(ix);
            if (!isFragmentViewBound(itemId)) {
                toRemove.add(itemId);
            }
        }
    }

    for (Long itemId : toRemove) {
        removeFragment(itemId);
    }
}

因为这个函数判断,之前 adapter 里面产生的 fragment 需要被回收,依据就是当前的 adatper.containsItem(id)的方法返回 false 了。再提供一个信息,这个函数会在 viewpager2 设置 adatper 的时候被调用。到现在为止,答案已经出来了。因为在 viewpager2 设置 adatper 的时候,adatper 里面什么数据也没有的啊,containsItem 函数必然返回为空的啊,真相大白了。

所以逻辑正确的代码应该如下:

val viewPager2: ViewPager2 = ......
model.getContentList.observe(viewLifecycleOwner) {
    if(viewPager2.adapter == null){
        val adapter: FragmentStateAdapter = ......
        adapter.data = it
        viewPager2.adapter = adapter
    } else {
        viewPager2.adapter.data = it
    }

    adapter.notifyDataSetChanged()
}

这段代码解决问题就是,在 viewpager2 设置 adatper 之前,先把 adapter 填充进去数据,然后再进行设置。这样就可以解决在 gcFragments 里面因为 containsItem()函数返回 false,导致 fragment 被销毁的问题,其实这个 fragment 是可以被重用的。

7. 在 Navigation 的框架下,手动进行 Fragment 管理需要注意什么?

刚开始使用 Navigation,代码里还是会有一部分 Fragment 是手动管理的,通过 FragmentManager 的 Add/Replace/Remove 等操作。其实 Navigation 设计的一部分初衷,就是要用统一的导航操作来替代手动操作,虽然 Navigation 底层的操作也是通过 FragmentManager 来实现的。

如果代码里面还是有手动管理的代码,需要特别注意就是手动操作的时机和方式。原因还是在 Navigation 的框架下,Fragment 和 View 的生命周期不一致导致的。如果把操作 Fragment 的时机放在 ViewLifeCycle 的里面,就可能会造成一些意想不到的结果。

假设 stack top-most 的页面返回之后,新处于 Stack 顶的页面,原先处于 Stop 的 Fragment 就会走 onStart,而整个 View 将会被重建。如果在 ViewLifeCycle 的生命周期里面去 Add or Replace 一个 fragment,就必须要判断,需要操作的 fragment 是否已经存在,如果这个 fragment 已经存在,又进行了一次 Add or Replace 操作,这个 fragment 将会被重建,原来的 fragment 将会被销毁,新的 fragment 会被创建,而 view 的生命周期将会走两次,导致不必要的性能损失。不仅仅是性能的损失,还会导致之前谈到的第 4 条问题,导致内存泄漏的问题。

即使是注意到了这些问题,如果手动来判断情况,也会造成代码不必要的复杂,所以还是建议使用 Navigation 的框架来导航,而不是手动通过 FragmentManager 来进行操作。

8. Navigation 的主持下,Fragment 和 View 分家了,家产怎么分?

最后其实是个设计问题。View 是依附于 Fragment,从 Fragment 的 create 到 destory 的一生中,可能会伴随着多个 View 的实例,从 create 走到 destory。

从而,我们需要考虑,哪些变量应该是属于 fragment 的,哪些变量应该属于 view 的。

举一个例子,如果一个页面有一个列表,使用 recycleView 来实现,毫无疑问,recycleView 是属于 View 的,但是这个 recycleView 的 adapter 呢?如果这个 adapter 属于 view,那 adapter 的实例将会随着这个 recycleview 的创建而创建,消亡而消亡。如果把这个 adapter 放到 fragment,不管有多少个 View 实例将会被创建,用的就是 fragment 里面的 adapter,这样的设计,是不是会更好,更少的代价来实现需求。

所以这个家产如何分,第一优先顺位是 fragment, 第二个顺位才是 view。放在 fragment 里面才会最大程度的重用对象,达到性能的最大化。

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/XdPYjC_6NP-0rmp8Y3hNpg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 相关文章
简化Android的UI开发 4年以前  |  520719次阅读
Android 深色模式适配原理分析 3年以前  |  28663次阅读
Android阴影实现的几种方案 1年以前  |  10820次阅读
Android 样式系统 | 主题背景覆盖 3年以前  |  9611次阅读
 目录