Android:在Activity上下文之外使用WebView

29

我正试图通过后台IntentService实现网页抓取,定期在用户手机上显示视图的情况下进行抓取.

  • 由于必须调用加载页面上的一些JavaScript代码,因此无法使用任何HttpGet等。
  • 因此,必须使用WebView实例,但它只能在UI线程上运行。
  • 任何尝试启动使用WebView的Activity都会导致View进入手机前景(根据Android的Activity设计)
  • 任何尝试在不属于Activity上下文的地方使用WebView都会导致错误,指出不能在非UI线程上使用WebView。
  • 由于各种复杂原因,我不能考虑使用Rhino等库进行无界面Web抓取。

有没有办法解决这个问题?

10个回答

44

你可以从服务中显示一个webview。以下代码创建了一个窗口,你的服务可以访问。该窗口不可见,因为大小为0乘以0。

public class ServiceWithWebView extends Service {

    @Override
    public void onCreate() {
        super.onCreate();

        WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        params = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
        params.gravity = Gravity.TOP | Gravity.LEFT;
        params.x = 0;
        params.y = 0;
        params.width = 0;
        params.height = 0;

        LinearLayout view = new LinearLayout(this);
        view.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT));

        WebView wv = new WebView(this);
        wv.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
        view.addView(wv);
        wv.loadUrl("http://google.com");

        windowManager.addView(view, params);
    }
}

还需要获得android.permission.SYSTEM_ALERT_WINDOW权限。


尽管如此,您仍在ACTIVITY类的onCreate方法中启动服务。我无法在活动中执行代码,因为活动将对用户可见。在不继承Activity部分的类中创建WebView的新实例将导致运行时异常。 - Pierre
我不确定我理解你评论的第一部分。是的,你需要找到一种启动服务的方式,但它可以是一个简单地启动然后结束的活动。至于WebView,我绝对知道这会起作用。这个“onCreate()”方法来自服务本身。我大约一周前自己做过这件事,从未遇到过运行时异常。我猜你尝试使用“onStartCommand()”函数,这可能会从不同的线程调用。 - Randy
1
如果我在一个活动中启动服务,那么用户至少会看到屏幕闪烁一下,对吧?它需要是不可见的。我已经成功地从IntentService启动了您的ServiceWithWebView示例,并通过AlarmManager每20秒调用一次。一切都执行得很好,除了当添加任何WebView代码时,我会收到异常“从Activity上下文之外调用startActivity()需要FLAG_ACTIVITY_NEW_TASK标志”。我认为这是因为我是从非UI线程启动的。我尝试在意图上添加FLAG_ACTIVITY_NEW_TASK。 - Pierre
2
我知道这是一个过时的回答,但为了将来参考:即使在KitKat中使用基于Chromium的WebView,此方法确实有效。 我正在使用它在通过AlarmManager定期启动的服务中。 但是,您不需要将WebView添加到LinearLayout中。 直接将其添加到WindowManager似乎也可以正常工作。 - David Brown
1
这个不行。最后一行会抛出一个异常,说“android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?”。 - Donald Duck
显示剩余11条评论

13

请纠正我,但是这个问题的正确答案是:没有可能在用户在手机上进行其他操作时在后台使用WebView而不通过Activity中断用户。

我已经尝试了Randy和Code_Yoga的建议:使用带有"Theme.NoDisplay"的活动来启动一个带有WebView的后台服务来完成一些工作。 但是,即使没有视图可见,切换到该活动以启动服务也会打断用户(例如,暂停正在进行的游戏)。

这对我的应用程序来说是完全灾难性的消息,因此我仍然希望有人能给我一种不需要Activity的WebView使用方法(或可以完成相同任务的WebView替代品)。


1
这是真的,你无法在ActivityContext之外正确地使用webview,Randy的答案可能是一个解决方案,但从来没有建议在所有其他应用程序的顶部创建窗口。 - Aaron
嗨,皮埃尔。我认为你对code_yoda的答案的评论今天不再有效,因为它需要在onResume()之前执行finish() https://commonsware.com/blog/2015/11/02/psa-android-6p0-theme.nodisplay-regression.html。然而,即使活动被销毁,与NoDisplay主题相关的webView客户端中仍然存在活动的上下文(我不知道是如何做到的)。你想对此发表评论吗?我也添加了一个替代答案,但如果基于code_yoda的解决方案有一些可行性,我认为我们可以围绕它构建一个解决方案。 - Shubham AgaRwal

8
你可以使用这个来隐藏 Activity。
         <activity android:name="MyActivity"
          android:label="@string/app_name"
          android:theme="@android:style/Theme.NoDisplay">

这样做将防止应用程序显示任何活动。然后您可以在Activity中完成您的工作。


1
该活动仍然“显示”,只是不太可见。关闭它并仍然拥有 WebView 的引用是否允许以后使用 WebView? - android developer

3

解决方案如下,但要使用 Looper.getMainLooper():

https://github.com/JonasCz/save-for-offline/blob/master/app/src/main/java/jonas/tool/saveForOffline/ScreenshotService.java

@Override
public void onCreate() {
    super.onCreate();
    //HandlerThread thread = new HandlerThread("ScreenshotService", Process.THREAD_PRIORITY_BACKGROUND);
    //thread.start();
    //mServiceHandler = new ServiceHandler(thread.getLooper()); // not working
    mServiceHandler = new ServiceHandler(Looper.getMainLooper()); // working
}

在@JonasCz的帮助下:https://dev59.com/J4fca4cB1Zd3GeqPeBQr#28234761


2
我使用了以下代码来解决这个问题:

我使用了以下代码来解决这个问题:

Handler handler = new Handler(Looper.getMainLooper());
try
{
    handler.post(
        new Runnable()
        {
            @Override
            public void run()
            {
                ProcessRequest(); // Where this method runs the code you're needing
            }
        }
    );
} catch (Exception e)
{
    e.printStackTrace();
}

1
由于 WebView 是一个 UI,所以它不能存在于 Activity 或 Fragment 之外。 但这意味着只需要创建 WebView 而不是处理其所有请求的 Activity。
如果您在主 Activity 中创建不可见的 WebView,并使其从静态上下文中可访问,则应该能够在后台从任何地方执行视图中的任务,因为我认为 WebView 的所有 IO 都是异步完成的。
为了消除全局访问的麻烦,您可以随时启动一个 Service,并引用 WebView 来执行所需的工作。

1
这难道不意味着你会有一个内存泄漏,因为你有一个 WebView 引用了这个 Activity 吗? - android developer

0
为什么不创建一个后端服务来帮你进行爬取呢?
然后你只需要从RESTful Web服务中轮询结果,甚至可以使用消息中间件(例如ZeroMQ)。
如果适用于您的用例,可能更加优雅:让爬取服务通过GCM向您的应用程序发送推送消息 :)

1
这是因为后端会被检测为从同一IP抓取数据的机器人,并被阻止(除了需要在不同页面上进行大量抓取的后端资源)。 - Pierre
因为并不是所有事情都发生在在线状态下。 - Benoit

0

我不确定这是否是解决问题的万能药。
根据@Pierre的已接受答案(在我看来是正确的)

在用户在手机上进行其他操作时,没有可能在后台使用WebView而不通过Activity打断用户。

因此,我相信必须进行某些架构/流程/策略更改才能解决此问题。

建议解决方案#1:而不是从服务器获取推送通知并运行后台作业,然后再运行一些JS代码或WebView,应该每当用户启动应用程序时查询后端服务器以了解是否需要执行任何抓取。根据后端输入,Android客户端可以运行JS代码或WebView并将结果传回服务器。

我尚未尝试此解决方案。但希望它是可行的。


这也将解决评论中提到的以下问题:
原因是后端会被检测为从同一IP抓取数据的机器人并被阻止(除了需要在不同页面上进行大量抓取的后端资源)。
数据可能在一段时间内不可用(直到某个用户为您抓取它)。但是,使用此策略,我们肯定可以为最终用户提供更好的用户体验。

0

或者使用替代方案来代替 WebView,以实现相同的功能 <=== 如果您不想在 UI 上显示加载的信息,可以尝试直接使用 HTTP 调用 URL,并处理从 HTTP 返回的响应


2
一些内容只能添加到WebView上并显示,因为它支持JavaScript,可以具有添加内容的功能。 - android developer

-1

我知道已经过去了一年半,但现在我面临着同样的问题。最终我通过在我的 Android 应用程序内运行一个运行在 Node 引擎中的 Javascript 代码来解决它。这个引擎叫做JXCore。你可以看一下。此外,还可以看一下这个示例,它可以在不使用 WebView 的情况下运行 Javascript。我真的很想知道你最终使用了什么?


两个链接都不工作,请修复后通知我。 - Dale

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接