Android以太网网口注册流程

2023-05-16


一 引言

  在上一篇文章,我们从上层APP出发,通过以太网的使能来分析了以太网框架中,上层指令如何传递到底层。这篇文章,我们将通过网口注册的流程来分析,以太网框架中,底层事件状态是如何上报给上层。图1-1所示为网口注册的整体流程图。

在这里插入图片描述

图1-1 网口注册整体流程图

二 Netd事件上报流程

  从上一节图1-1可以看出,Netd是通过SocketListener来监听底层的Uevent的事件上报。在分析Netd事件上报之前,有必要先将Netd中相关的类的关系梳理一下,具体类图如下2-1所示:
在这里插入图片描述

图2-1 Netd相关类图关系

从Netd的类图关系看,主要有4个类,其主要作用如下:

  • SocketListener : 这是Native层获取底层Uevent的公用接口类,Android其他一些模块,例如vold模块,也通过该Listener来监听底层Uevent事件。监听到事件后,通过onDataAvailable来处理底层上报的数据;
  • NetlinkListener:该类继承自SocketListener。在onDataAvailable函数中调用onEvent继续封装底层的事件;
  • NetlinkHandler:该类继承自NetlinkListener。在onEvent中继续封装处理底层事件,并通过上层注册的监听对象,将该状态上报给上层。
  • NetlinkManager:其内部维护了几个NetinkHandler引用对象,以及一个SocketListener引用对象,来管理socket的通信,包括socket的创建,以及释放等等。

  Netd在启动时候,会创建NetlinkManager引用对象,并且创建NetlinkHandler的引用对象,并且调用其start来启动监听,最后会调用到SocketListener的runListener函数中,来实时监听底层Uevent事件上报。

//SocketListener.cpp
void SocketListener::runListener() {
    while (true) {
        std::vector<pollfd> fds;

        pthread_mutex_lock(&mClientsLock);
        fds.reserve(2 + mClients.size());
        fds.push_back({.fd = mCtrlPipe[0], .events = POLLIN});
        if (mListen) fds.push_back({.fd = mSock, .events = POLLIN});
        for (auto pair : mClients) {
            // NB: calling out to an other object with mClientsLock held (safe)        
            const int fd = pair.second->getSocket();
            if (fd != pair.first) SLOGE("fd mismatch: %d != %d", fd, pair.first);      
            fds.push_back({.fd = fd, .events = POLLIN});
        }
        pthread_mutex_unlock(&mClientsLock);

        SLOGV("mListen=%d, mSocketName=%s", mListen, mSocketName);                     
        int rc = TEMP_FAILURE_RETRY(poll(fds.data(), fds.size(), -1));                 
        if (rc < 0) {
            SLOGE("poll failed (%s) mListen=%d", strerror(errno), mListen);            
            sleep(1);
            continue;
        }

        if (fds[0].revents & (POLLIN | POLLERR)) {
            char c = CtrlPipe_Shutdown;
            TEMP_FAILURE_RETRY(read(mCtrlPipe[0], &c, 1));
            if (c == CtrlPipe_Shutdown) {
                break;
            }
            continue;                                                                  
        }
        if (mListen && (fds[1].revents & (POLLIN | POLLERR))) {
            int c = TEMP_FAILURE_RETRY(accept4(mSock, nullptr, nullptr, SOCK_CLOEXEC));
            if (c < 0) {
                SLOGE("accept failed (%s)", strerror(errno));
                sleep(1);
                continue;
            }
            pthread_mutex_lock(&mClientsLock);
            mClients[c] = new SocketClient(c, true, mUseCmdNum);
            pthread_mutex_unlock(&mClientsLock);
        }

        // Add all active clients to the pending list first, so we can release         
        // the lock before invoking the callbacks.                                     
        std::vector<SocketClient*> pending;
        pthread_mutex_lock(&mClientsLock);
        const int size = fds.size();
        for (int i = mListen ? 2 : 1; i < size; ++i) {
            const struct pollfd& p = fds[i];
            if (p.revents & (POLLIN | POLLERR)) {
                auto it = mClients.find(p.fd);
                if (it == mClients.end()) {
                    SLOGE("fd vanished: %d", p.fd);
                    continue;
                }
                SocketClient* c = it->second;
                pending.push_back(c);
                c->incRef();
            }
        }
        pthread_mutex_unlock(&mClientsLock);

        for (SocketClient* c : pending) {
            // Process it, if false is returned, remove from the map                   
            SLOGV("processing fd %d", c->getSocket());
            //通过onDataAvailable函数处理底层事件的上报
            if (!onDataAvailable(c)) {
                release(c, false);
            }
            c->decRef();
        }   
    }
}

  在runListener中是一个死循环,实时监听底层的Uevent事件。在接收到底层事件上报后,最终调用onDataAvailable处理,真正实现是在子类NetLinkListener中,具体代码如下:

//NetlinkListener.cpp
bool NetlinkListener::onDataAvailable(SocketClient *cli)                        
{                                                                               
    int socket = cli->getSocket();                                              
    ssize_t count;                                                              
    uid_t uid = -1;                                                             
                                                                                
    bool require_group = true;                                                  
    if (mFormat == NETLINK_FORMAT_BINARY_UNICAST) {                             
        require_group = false;                                                  
    }                                                                           
    //通过uevent_kernel_recv获取底层Uevent事件,并将其存储到mBuffer中
    count = TEMP_FAILURE_RETRY(uevent_kernel_recv(socket,                       
            mBuffer, sizeof(mBuffer), require_group, &uid));                    
    if (count < 0) {                                                            
        SLOGE("recvmsg failed (%s)", strerror(errno));                          
        return false;                                                           
    }                                                                           
    //创建NetlinkEvent对象,并且解析后,最终调用onEvent做进一步处理
    NetlinkEvent *evt = new NetlinkEvent();                                     
    if (evt->decode(mBuffer, count, mFormat)) {                                 
        onEvent(evt);                                                           
    } else if (mFormat != NETLINK_FORMAT_BINARY) {                              
        // Don't complain if parseBinaryNetlinkMessage returns false. That can  
        // just mean that the buffer contained no messages we're interested in. 
        SLOGE("Error decoding NetlinkEvent");                                   
    }                                                                           
                                                                                
    delete evt;                                                                 
    return true;                                                                
}                                                                               

在NetLinkListener中的onDataAvailable方法中,主要做了以下三件事:

  • 通过uevent_kernel_recv获取底层的Uevent事件的上报,并将其存储到mBuffer中;
  • 创建NetlinkEvent对象,并调用decode函数,将mBuffer中的数据风封装到NetlinkEvent中;
  • 最终调用子类的onEvent继续处理;
 bool NetlinkEvent::decode(char *buffer, int size, int format) {           
     if (format == NetlinkListener::NETLINK_FORMAT_BINARY                  
             || format == NetlinkListener::NETLINK_FORMAT_BINARY_UNICAST) {
         return parseBinaryNetlinkMessage(buffer, size);                   
     } else {                                                              
         return parseAsciiNetlinkMessage(buffer, size);                    
     }                                                                     
 }                                                                         

在decode函数中,根据底层上报的是二进制数据做不通的处理,如果是二进制数据,则调用parseBinaryNetlinkMessage函数处理;否则调用parseAsciiNetlinkMessage函数处理。二进制主要是路由等信息的上报,此处我们重点关注非二进制的数据上报。

bool NetlinkEvent::parseAsciiNetlinkMessage(char *buffer, int size) {             
    const char *s = buffer;                                                       
    const char *end;                                                              
    int param_idx = 0;                                                            
    int first = 1;                                                                
                                                                                  
    if (size == 0)                                                                
        return false;                                                             
                                                                                  
    /* Ensure the buffer is zero-terminated, the code below depends on this */    
    buffer[size-1] = '\0';                                                        
                                                                                  
    end = s + size;                                                               
    while (s < end) {                                                             
        if (first) {                                                              
            const char *p;                                                        
            /* buffer is 0-terminated, no need to check p < end */                
            for (p = s; *p != '@'; p++) {                                         
                if (!*p) { /* no '@', should not happen */                        
                    return false;                                                 
                }                                                                 
            }                                                                     
            mPath = strdup(p+1);                                                  
            first = 0;                                                            
        } else {                                                                  
            const char* a;
            //解析ACTION属性并将其存储到NetlinkEvent的mAction中
            if ((a = HAS_CONST_PREFIX(s, end, "ACTION=")) != nullptr) {           
                if (!strcmp(a, "add"))
                    //如果是网口的注册,此处动作是add
                    mAction = Action::kAdd;                                       
                else if (!strcmp(a, "remove"))                                    
                    mAction = Action::kRemove;                                    
                else if (!strcmp(a, "change"))                                    
                    mAction = Action::kChange;                                    
            } else if ((a = HAS_CONST_PREFIX(s, end, "SEQNUM=")) != nullptr) {    
                mSeq = atoi(a);                                                   
            } else if ((a = HAS_CONST_PREFIX(s, end, "SUBSYSTEM=")) != nullptr) { 
                mSubsystem = strdup(a);                                           
            } else if (param_idx < NL_PARAMS_MAX) {                               
                mParams[param_idx++] = strdup(s);                                 
            }                                                                     
        }                                                                         
        s += strlen(s) + 1;                                                       
    }                                                                             
    return true;                                                                  
}                                                                                 

  在parseAsciiNetlinkMessage中,主要解析ACTION=参数,并将其存储到NetlinkEvent的mAction中。下面我们直接看NetlinkListener的子类NetlinkHandler的onEvent中处理,我们直接看代码:

//NetlinkHandler.cpp
void NetlinkHandler::onEvent(NetlinkEvent *evt) {
    const char *subsys = evt->getSubsystem();
    if (!subsys) {
        ALOGW("No subsystem found in netlink event");
        return;
    }
    //解析net参数,再根据action的不同做不同的处理
    if (!strcmp(subsys, "net")) {
        NetlinkEvent::Action action = evt->getAction();
        const char *iface = evt->findParam("INTERFACE");
        if ((action == NetlinkEvent::Action::kAdd) ||
            (action == NetlinkEvent::Action::kLinkUp) ||
            (action == NetlinkEvent::Action::kLinkDown)) {
            const char *ifIndex = evt->findParam("IFINDEX");
            long ifaceIndex = parseIfIndex(ifIndex);
            if (ifaceIndex) {
                //先根据iface做addInterface处理
                gCtls->trafficCtrl.addInterface(iface, ifaceIndex);
            } else {
                ALOGE("invalid interface index: %s(%s)", iface, ifIndex);
            }
        }
        //通知上层,底层状态发生变化
        if (action == NetlinkEvent::Action::kAdd) {
            notifyInterfaceAdded(iface);
        } else if (action == NetlinkEvent::Action::kRemove) {
        ......
      }
}

NetlinkHandler的onEvent函数主要完成两件事:

  • 通过trafficCtrl的addInterface函数创建注册的interface;
  • 根据Action,调用notifyInterfaceAdded函数,通知上层状态的变化;

我们直接看notifyInterfaceAdded函数的实现:

void NetlinkHandler::notifyInterfaceAdded(const std::string& ifName) {
    LOG_EVENT_FUNC(BINDER_RETRY, onInterfaceAdded, ifName);
}
//宏函数定义,获取每个注册NetdUnsolicitedListener,然后调用其函数onInterfaceAdded
#define LOG_EVENT_FUNC(retry, func, ...)                                                    \ 
    do {                                                                                    \ 
        const auto listenerMap = gCtls->eventReporter.getNetdUnsolicitedEventListenerMap(); \ 
        for (auto& listener : listenerMap) {                                                \ 
            auto entry = gUnsolicitedLog.newEntry().function(#func).args(__VA_ARGS__);      \ 
            if (retry(listener.first->func(__VA_ARGS__))) {                                 \ 
                gUnsolicitedLog.log(entry.withAutomaticDuration());                         \ 
            }                                                                               \ 
        }                                                                                   \ 
    } while (0)                                                                               

  这里通过宏函数,获取每个注册的NetdUnsolicitedListener,然后调用对应的函数,此处为onInterfaceAdded。在Android以太网服务启动源码分析中,我们知道在NetworkManagementService的connectNativeService中注册了NetdUnsolicitedListener。所以下面将会进入framework层的处理。这里我们最后在分析一下getNetdUnsolicitedEventListenerMap的注册和获取。

EventReporter::UnsolListenerMap EventReporter::getNetdUnsolicitedEventListenerMap() const {
    std::lock_guard lock(mUnsolicitedMutex);                                               
    return mUnsolListenerMap;
}
//上层调用NetdUnsolicitedListener的注册,最终会走到这里,
//并将注册的Listener添加到mUnsolListenerMap中
void EventReporter::registerUnsolEventListener(                                     
        const android::sp<INetdUnsolicitedEventListener>& listener) {               
    std::lock_guard lock(mUnsolicitedMutex);                                        
                                                                                    
    // Create the death listener.                                                   
    class DeathRecipient : public android::IBinder::DeathRecipient {                
      public:                                                                       
        DeathRecipient(EventReporter* eventReporter,                                
                       android::sp<INetdUnsolicitedEventListener> listener)         
            : mEventReporter(eventReporter), mListener(std::move(listener)) {}      
        ~DeathRecipient() override = default;                                       
        void binderDied(const android::wp<android::IBinder>& /* who */) override {  
            mEventReporter->unregisterUnsolEventListener(mListener);                
        }                                                                           
                                                                                    
      private:                                                                      
        EventReporter* mEventReporter;                                              
        android::sp<INetdUnsolicitedEventListener> mListener;                       
    };                                                                              
    android::sp<android::IBinder::DeathRecipient> deathRecipient =                  
            new DeathRecipient(this, listener);                                     
                                                                                    
    android::IInterface::asBinder(listener)->linkToDeath(deathRecipient);           
                                                                                    
    // TODO: Consider to use remote binder address as registering key               
    mUnsolListenerMap.insert({listener, deathRecipient});                           
}                                                                                   

  至此,Netd的流程就分析完了,下面我们来分析Framework层的处理。

三 Framework处理事件上报流程

  上一节我们知道,最后底层的事件,回调到NetworkManagementService的NetdUnsolicitedEventListener中。我们直接看NetdUnsolicitedEventListener的回调函数:

//NetworkManagementService.java
private class NetdUnsolicitedEventListener extends INetdUnsolicitedEventListener.Stub {
    ......
     @Override
     public void onInterfaceAdded(String ifName) throws RemoteException {   
        mDaemonHandler.post(() -> notifyInterfaceAdded(ifName));
     }

    @Override                                                              
     public void onInterfaceRemoved(String ifName) throws RemoteException { 
        mDaemonHandler.post(() -> notifyInterfaceRemoved(ifName));         
     }
    ......
}
private void notifyInterfaceAdded(String iface) {
    Slog.d(TAG, "notifyInterfaceAdded iface: " + iface);
    invokeForAllObservers(o -> o.interfaceAdded(iface)); 
}                                                        

上层回调到NetdUnsolicitedEventListener的onInterfaceAdded方法中,最后调用notifyInterfaceAdded方法做处理。该方法很简单直接调用invokeForAllObservers通知注册的Observers。通过Android以太网服务启动源码分析可以知道,EthernetTracker中注册了该Observer,我们直接看该回调。

//EthernetTracker.java
private class InterfaceObserver extends BaseNetworkObserver {
    @Override
    public void interfaceAdded(String iface) {
       mHandler.post(() -> maybeTrackInterface(iface));
    }
}

private void maybeTrackInterface(String iface) {                               
    if (DBG) Log.i(TAG, "maybeTrackInterface " + iface);                       
    // If we don't already track this interface, and if this interface matches 
    // our regex, start tracking it.                                           
    if (!iface.matches(mIfaceMatch) || mFactory.hasInterface(iface)) {         
        return;                                                                
    }                                                                          

    if (mIpConfigForDefaultInterface != null) {
        //更新ip Config
        updateIpConfiguration(iface, mIpConfigForDefaultInterface);            
        mIpConfigForDefaultInterface = null;                                   
    }                                                                          
    //根据iface,调用addInterface创建interface
    addInterface(iface);                                                       
}                                                                              

EthernetTracker中主要做了以下两件事

  • 首先更新 ip config的,这个和静态ip相关;
  • 根据iface,调用addInterface创建interface
 private void addInterface(String iface) {                                                     
     InterfaceConfiguration config = null;                                                     
     // Bring up the interface so we get link status indications.                              
     try {
         //调用NetworkManagementService的setInterfaceUp使能interface
         mNMService.setInterfaceUp(iface);
         //从Netd获取config信息,主要通过该config获取mac地址
         config = mNMService.getInterfaceConfig(iface);
     } catch (RemoteException | IllegalStateException e) {
         // Either the system is crashing or the interface has disappeared. Just ignore the    
         // error; we haven't modified any state because we only do that if our calls succeed. 
         Log.e(TAG, "Error upping interface " + iface, e);                                     
     }
     final String hwAddress = config.getHardwareAddress();
     //创建网络能力的管理类
     NetworkCapabilities nc = mNetworkCapabilities.get(iface);
     if (nc == null) {
         // Try to resolve using mac address                                                   
         nc = mNetworkCapabilities.get(hwAddress);                                             
         if (nc == null) {                                                                     
             nc = createDefaultNetworkCapabilities();                                          
         }                                                                                     
     }                                                                                         
     IpConfiguration ipConfiguration = mIpConfigurations.get(iface);                           
     if (ipConfiguration == null) {                                                            
         ipConfiguration = createDefaultIpConfiguration();                                     
     }                                                                                         
                                                                                               
     Log.d(TAG, "Started tracking interface " + iface); 
     //调用EthernetNetworkFactory的addInterface增加网络注册
     mFactory.addInterface(iface, hwAddress, nc, ipConfiguration);                             
                                                                                               
     // Note: if the interface already has link (e.g., if we crashed and got                   
     // restarted while it was running), we need to fake a link up notification so we          
     // start configuring it.                                                                  
     if (config.hasFlag("running")) {
         //通知app注册的listener,更新interface的状态
         updateInterfaceState(iface, true);                                                    
     }
 }

addInterface主要完成以下几件事:

  • 调用NMService的setInterfaceUp方法,使能以外网功能;
  • 调用NMService的getInterfaceConfig获取interface config,并通过其获取硬件mac地址;
  • 通过iface从mNetworkCapabilities获取网路能力的类——NetworkCapabilities,如果没有,则通过硬件mac地址获取NetworkCapabilities;如果还没有,则创建默认的NetworkCapabilities;
  • 调用EthernetNetworkFactory的addInterface增加网络注册,更新网络能力,ip配合等信息;
  • 通知app注册的Listener,更新interface的状态。
private void updateInterfaceState(String iface, boolean up) {
    //调用updateInterfaceLinkState更新连接状态,最终更新到ConnectService中通知网络状态的改变
    boolean modified = mFactory.updateInterfaceLinkState(iface, up);
    if (modified) {
        boolean restricted = isRestrictedInterface(iface);                                      
        int n = mListeners.beginBroadcast();                                                    
        for (int i = 0; i < n; i++) {                                                           
            try {                                                                               
                if (restricted) {                                                               
                    ListenerInfo listenerInfo = (ListenerInfo) mListeners.getBroadcastCookie(i);
                    if (!listenerInfo.canUseRestrictedNetworks) {                               
                        continue;                                                               
                    }
                }
                //最终调用listener的onAvailabilityChanged方法,更新状态
                mListeners.getBroadcastItem(i).onAvailabilityChanged(iface, up);                
            } catch (RemoteException e) {                                                       
                // Do nothing here.                                                             
            }                                                                                   
        }
        mListeners.finishBroadcast();                                                           
    }
}

EthernetManager注册了该Listener,我们直接看EthernetManager的处理:

//EthernetManager。java
private final Handler mHandler = new Handler() {
     @Override
     public void handleMessage(Message msg) {
         //发送MSG_AVAILABILITY_CHANGED消息,回调对应listener的onAvailabilityChanged
         if (msg.what == MSG_AVAILABILITY_CHANGED) {
             boolean isAvailable = (msg.arg1 == 1);
             for (Listener listener : mListeners) {
                 listener.onAvailabilityChanged((String) msg.obj, isAvailable);
             }                                                                 
         }
     }
 };
//收到底层的状态改变后,回调到此处,并且通过handler,发送MSG_AVAILABILITY_CHANGED消息处理
private final IEthernetServiceListener.Stub mServiceListener =
         new IEthernetServiceListener.Stub() {
             @Override
             public void onAvailabilityChanged(String iface, boolean isAvailable) {              
                 mHandler.obtainMessage(
                         MSG_AVAILABILITY_CHANGED, isAvailable ? 1 : 0, 0, iface).sendToTarget();
             }
         };
//应用层通过addListener,添加Listener,最后通过onAvailabilityChanged回调,网线插拔的动作
public void addListener(Listener listener) {
    mListeners.add(listener);
    if (mListeners.size() == 1) {                                       
        try {
            mService.addListener(mServiceListener);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }                                                               
    }
}

  在EthernetManager中,当EthernetTracker中有底层状态信息改变的时候,会回调到IEthernetServiceListener的onAvailabilityChanged中。通过Handler发送MSG_AVAILABILITY_CHANGED消息,并且循环回调每个Listener的onAvailabilityChanged中,而Listener是通过应用调用addListener注册的。

四 总结

  至此,我们以太网框架源码分析的系列就结束了,其中有很多我们需要关注细小知识点。我们分析了以太网服务的启动,以及底层和上层之间的消息通信。有分析不到位的,欢迎大家一块讨论。敬请期待后面其他模块的源码分析。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android以太网网口注册流程 的相关文章

  • Android 从 C++ 端播放原始音频

    我需要能够在 Android 系统的 C 端以自定义文件格式传输音频 我正在致力于移植自定义媒体播放器 并且需要能够打开自定义文件并从中传输音频 这很重要 因为我认为从性能角度来看将整个播放器移植到 JAVA 是不可行的 并且通过 JNI
  • Android Studio Beta 频道、Android Studio Canary 频道、Android Studio Dev 频道有什么区别? [关闭]

    Closed 这个问题需要细节或清晰度 help closed questions 目前不接受答案 我是 android 新手 想知道要安装哪个 studio Android Studio Beta 频道 Android Studio Ca
  • 如何在 M1 Mac 上运行的模拟器上运行旧版 Android 版本(例如 API 级别 21)?

    虽然现在有一个适用于 M1 mac 的 Android Studio 和支持arm架构的Android模拟器镜像 https stackoverflow com questions 64907154 android studio emula
  • 如何实现 ALTER TABLE 的示例[重复]

    这个问题在这里已经有答案了 我已经多次问过这个问题 但尚未得到完整的答案 如何实现 ALTER TABLE 语句以向数据库添加列 有人可以给我举个例子吗 请阅读SQLite ALTER TABLE 参考 http sqlite org la
  • Android studio - 如何保存先前活动中选择的数据

    这是我的代码片段 这Textview充当按钮并具有Onclicklistner在他们 当cpu1000时Textview单击它会导致cpu g1000其代码如下所示的类 public class Game 1000 extends AppC
  • SQLite FTS4 使用特殊字符进行搜索

    我有一个 Android 应用程序 它使用 FTS4 虚拟表在 SQLite 数据库中搜索数据 它工作正常 但是当表中的数据包含特殊字符 如 或 时 SQLite MATCH 函数不会给出任何结果 我现在迷路了 谢谢 注意 默认的分词器真的
  • 在 /dev/input/eventX 中写入事件需要哪些命令?

    我正在开发一个android需要将触摸事件发送到 dev input eventX 的应用程序 我知道C执行此类操作的代码结构如下 struct input event struct timeval time unsigned short
  • 如何向 Android Studio 中的现有项目添加新活动?

    在 Eclipse 中 您只需单击 新建 按钮并选择 Android 活动即可添加新活动 但 Android Studio 有点不同 我无法找到如何向项目添加新活动 要添加一个Activity使用 Android Studio 此步骤与添加
  • TextView 宽度匹配drawableTop 宽度

    有什么办法可以使TextView width匹配复合可绘制宽度 XML 例如对于 xml 代码
  • 在 android studio 中找不到 SDK 位置

    我刚刚在 android studio 中导入了我的 eclipse 项目 我一直这么说 Error SDK location not found Define location with sdk dir in the local prop
  • 如何将设备屏幕位置转换为发送事件位置?

    我知道关于input tap x yshell 命令 但是 我想了解如何 使用执行单击sendevent命令 我能够通过以下命令实现它 sendevent dev input event5 3 53 X sendevent dev inpu
  • 如何仅从 Firestore 获取最新更新的数据?

    在 Firestore 上发现任何更改时始终获取整个文档 如何只获取最近更新的数据 这是我的数据 我需要在第一次加载时在聊天中按对象顺序 例如 2018 09 17 30 40 msg和sendby 并且如果数据更新则仅获取新的msg和se
  • 带有 backstack Resume 的嵌套片段

    在我的应用程序中有几个fragments in an activity我正在维护一个backStack对于这些fragment 一切都很好 但其中有一个嵌套的片段 当我把它放入backStack然后再次按后退按钮恢复 该片段看起来与先前的内
  • Android应用主题更换流畅

    我正在开发一个提供白天和夜间主题的项目 我正在更改主题 夜间主题 AppCompatDelegate setDefaultNightMode AppCompatDelegate MODE NIGHT YES 日主题 AppCompatDel
  • 蓝牙发送和接收文本数据

    我是 Android 开发新手 我想制作一个使用蓝牙发送和接收文本的应用程序 我得到了有关发送文本的所有内容逻辑工作 但是当我尝试在手机中测试它时 我看不到界面 这是Main Activity Code import android sup
  • Android 4.4 Kitkat 自定义视图操作栏未填充整个宽度

    我试图拥有一个带有自定义视图的简单操作栏 但我得到以下结果 为了演示 我创建了一个带有黄色背景颜色的简单 xml 它应该占据整个宽度 这是 XML
  • 找不到数据库路径是不可能的

    我对 android 开发很陌生 现在我正在尝试通过扩展 SQLiteOpenHelper 的类创建数据库 我确信数据存储在我的 Nexus 7 我用来测试应用程序的设备 上的某个位置 但是我找不到数据库的路径 我四处寻找其他类似的问题 所
  • 在线性布局内的 ScrollView 内并排对齐 TextView

    我有一个带有滚动视图的线性布局 我想保留它的当前格式 但只需将 textView2a 和 textView3a 并排放置 而不会破坏我当前的布局格式 我已经包含了我最近的尝试 但它们似乎不正确 提前致谢 Java菜鸟 当前有效的 XML
  • View.post(),以及当Runnables被执行时

    我最初的问题是需要知道我的根的高度和宽度View这样我就可以进行程序化的布局更改 就我的目的而言 我不一定需要在onCreate 对于我来说 以编程方式添加我的孩子就足够了View根布局完成后 因此我很乐意使用onWindowFocusCh
  • Keystore getEntry 在 Android 9 上返回 NULL

    c我已对存储在 Android 密钥库中的登录密码进行了加密和解密 在 Android 9 上 我观察到应用程序在尝试解密密码时崩溃 我无法重现它 但拥有 Pixel 3 的用户是崩溃的设备之一 下面是我如何从密钥库解密密码 private

随机推荐

  • 利用XML文件的一个写日志的类!!!!!

    对于程序执行期间的错误跟踪 xff01 相信大家都有自己的一套办法 xff01 xff01 xff01 但都是利用文件文件 xff0c 我这次利用的是XML amp XSL xff0c 可产生报表格式的日志 轻松生成报表 xff01 xff
  • 解决golang获取时间默认使用UTC

    在Go语言上 xff0c go语言的time Now 返回的是当地时区时间 xff0c 直接用 xff1a time Now Format 2006 01 02 15 04 05 输出的是当地时区时间 但是部署之后 xff0c 有的服务器会
  • Android 系统 Settings 启动流程详解

    Settings简介 Settings 是 Android 系统自带的一个很重要的应用 xff0c 给用户提供了操作 Android 系统功能的界面 它里面包含了 Wireless amp network xff0c device xff0
  • Lucene学习总结之一:全文检索的基本原理

    一 总论 根据http lucene apache org java docs index html 定义 xff1a Lucene 是一个高效的 xff0c 基于Java 的全文检索库 所以在了解Lucene之前要费一番工夫了解一下全文检
  • Java发送http请求,参数形式为json

    Java发送http请求 xff0c 参数形式为json 不介绍知识 xff0c 哪不懂自己搜 只记录平时用到的 xff0c 可以给其他人做参考 需要传递的参数为json形式 xff0c 比如手机号 phone xff1a 123456 s
  • maven idea设置查找依赖优先从指定的本地仓库获取

    maven idea设置查找依赖优先从指定的本地仓库获取 选择Setting gt Build Execution Deployment gt Build Tools gt Maven gt Runner xff0c 设置Maven启动虚拟
  • eslint规则总结

    span class token string 34 off 34 span or span class token number 0 span span class token operator span 关闭规则 span class
  • 第4章 系统“后悔药”--vmware的快照功能

    通过前面几章的学习 xff0c 相信大家对centos有了一定的认识了吧 xff1f 我们的centos8 5是安装在vmware虚拟机上的 xff0c vmware虚拟机有一个非常实用的功能那就是快照功能 快照是个什么东西呢 xff1f
  • JDK 的 ORACLE 官网下载步骤

    下面以下载 jdk 8u181 linux x64 tar gz 为例 xff0c 说明下在 ORACLE官网下载的具体步骤 1 进入oracle官网 gt Resource gt Software Downloads xff0c 如下截图
  • 漫谈程序员系列:程序员的生活就这样吗

    我当了快十年程序员了 xff0c 终于老得可以来谈谈程序员的生活是什么样子了 或许陈奕迅的 十年 中的一段歌词 xff0c 可以表示很多程序员和软件开发之间的感情纠葛 xff1a 十年之前 我不认识你 你不属于我 我们还是一样 陪在一个陌生
  • 程序员转行为什么这么难

    尽管我在 大龄程序员的未来在何方 这篇文章里比较乐观地介绍了程序员保持竞争力的几个方向 xff0c 但现实依然是残酷的 xff1a 很多人将不得不离开软件开发工作 xff0c 转型去从事其他职业 当你要这么做时 xff0c 就会感慨 xff
  • Activity的onNewIntent()步骤何时会被调用及activity四种启动模式

    Activity的onNewIntent 方法何时会被调用 OnNewIntent被调用的前提是 ActivityA已经启动过 处于当前应用的Activity堆栈中 当ActivityA的LaunchMode为SingleTop时 xff0
  • php base64保存为图片

    前端传来的格式如下 xff1a POST 39 goodImage 39 61 data image png base64 iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAABFWlDQ1BpY2
  • linux中用crontab定时任务启动jar无效的问题

    修改前脚本内容如下 xff1a nohup java Xms512m Xmx512m jar mnt jar xx 0 0 1 SNAPSHOT jar amp 检查了权限等各方面可能 xff0c 一直都是脚本能执行 xff0c 但是不产生
  • 关于ubuntu安装过程中,分盘出现EFI分区错误问题解决方法

    步骤 Universal USB exe工具制作Ubuntu16 04 iso镜像安装 重启系统并按F12进入快捷启动界面 在启动过程中不选择UEFI启动项 xff0c 选择另一种启动方式即可
  • Ubuntu 设置 SSH 通过密钥登录

    Ubuntu 设置 SSH 通过密钥登录 我们一般使用 VSCode MobaXterm PuTTY等 SSH 客户端来远程管理 Linux 服务器 但是 xff0c 一般的密码方式登录 xff0c 容易有密码被暴力破解的问题 所以 xff
  • Windows_MySQL_8.0 _安装与卸载(压缩包)

    1 把 my ini 文件放到D Users admin Downloads MySQL mysql 8 0 31 winx64 my ini span class token punctuation span client span cl
  • 使用命令行启动 VirtualBox 虚拟机

    查看所有虚拟机 34 D Program Files Oracle VirtualBox VBoxManage exe 34 list vms 查看正在运行的虚拟机 34 D Program Files Oracle VirtualBox
  • centos8.5.2111更换阿里yum源

    本来不想写关于更换yum源的博客的 xff0c 可是最近再更换阿里yum源时出现了些问题 xff0c 网上的几篇博客又有误导新手之嫌疑 xff0c 所以就有了下面的这篇博客 1 使用root用户 xff0c 进入 etc yum repos
  • Android以太网网口注册流程

    一 引言 在上一篇文章 xff0c 我们从上层APP出发 xff0c 通过以太网的使能来分析了以太网框架中 xff0c 上层指令如何传递到底层 这篇文章 xff0c 我们将通过网口注册的流程来分析 xff0c 以太网框架中 xff0c 底层